Skip to main content

icydb_core/db/
identity.rs

1#![allow(clippy::cast_possible_truncation)]
2//! Identity invariants and construction.
3//!
4//! - Identities are ASCII, non-empty, and bounded by `MAX_*` limits.
5//! - Public constructors are the only validation boundary.
6//! - `*_unchecked` constructors are crate-private for macro-generated schemas;
7//!   executor paths assume identities are valid and do not re-validate.
8use crate::MAX_INDEX_FIELDS;
9use std::{
10    cmp::Ordering,
11    fmt::{self, Display},
12};
13use thiserror::Error as ThisError;
14
15///
16/// Constants
17///
18
19pub const MAX_ENTITY_NAME_LEN: usize = 64;
20pub const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
21pub const MAX_INDEX_NAME_LEN: usize =
22    MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
23
24///
25/// EntityNameError
26/// Errors returned when constructing an [`EntityName`].
27///
28
29#[derive(Debug, ThisError)]
30pub enum EntityNameError {
31    #[error("entity name is empty")]
32    Empty,
33
34    #[error("entity name length {len} exceeds max {max}")]
35    TooLong { len: usize, max: usize },
36
37    #[error("entity name must be ASCII")]
38    NonAscii,
39}
40
41///
42/// IndexNameError
43/// Errors returned when constructing an [`IndexName`].
44///
45
46#[derive(Debug, ThisError)]
47pub enum IndexNameError {
48    #[error("index has {len} fields (max {max})")]
49    TooManyFields { len: usize, max: usize },
50
51    #[error("index field name '{field}' exceeds max length {max}")]
52    FieldTooLong { field: String, max: usize },
53
54    #[error("index field name '{field}' must be ASCII")]
55    FieldNonAscii { field: String },
56
57    #[error("index name length {len} exceeds max {max}")]
58    TooLong { len: usize, max: usize },
59}
60
61///
62/// EntityName
63///
64
65#[derive(Clone, Copy, Eq, Hash, PartialEq)]
66pub struct EntityName {
67    pub len: u8,
68    pub bytes: [u8; MAX_ENTITY_NAME_LEN],
69}
70
71impl EntityName {
72    pub const STORED_SIZE: u32 = 1 + MAX_ENTITY_NAME_LEN as u32;
73    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
74
75    /// Build an entity name from a static identifier.
76    pub fn try_from_static(name: &'static str) -> Result<Self, EntityNameError> {
77        Self::try_from_str(name)
78    }
79
80    #[must_use]
81    /// Build an entity name from a validated static identifier.
82    ///
83    /// Caller must uphold all identity invariants; intended for generated code.
84    pub(crate) const fn from_static_unchecked(name: &'static str) -> Self {
85        let bytes = name.as_bytes();
86        let len = bytes.len();
87
88        let mut out = [0u8; MAX_ENTITY_NAME_LEN];
89        let mut i = 0;
90        while i < len {
91            let b = bytes[i];
92            out[i] = b;
93            i += 1;
94        }
95
96        Self {
97            len: len as u8,
98            bytes: out,
99        }
100    }
101
102    /// Build an entity name from runtime input, returning a typed error on failure.
103    pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
104        let bytes = name.as_bytes();
105        let len = bytes.len();
106
107        if len == 0 {
108            return Err(EntityNameError::Empty);
109        }
110        if len > MAX_ENTITY_NAME_LEN {
111            return Err(EntityNameError::TooLong {
112                len,
113                max: MAX_ENTITY_NAME_LEN,
114            });
115        }
116        if !bytes.is_ascii() {
117            return Err(EntityNameError::NonAscii);
118        }
119
120        let mut out = [0u8; MAX_ENTITY_NAME_LEN];
121        out[..len].copy_from_slice(bytes);
122
123        Ok(Self {
124            len: len as u8,
125            bytes: out,
126        })
127    }
128
129    #[must_use]
130    pub const fn len(&self) -> usize {
131        self.len as usize
132    }
133
134    #[must_use]
135    pub const fn is_empty(&self) -> bool {
136        self.len == 0
137    }
138
139    #[must_use]
140    pub fn as_bytes(&self) -> &[u8] {
141        &self.bytes[..self.len()]
142    }
143
144    #[must_use]
145    pub fn as_str(&self) -> &str {
146        // Safe because we enforce ASCII
147        unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
148    }
149
150    #[must_use]
151    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
152        let mut out = [0u8; Self::STORED_SIZE_USIZE];
153        out[0] = self.len;
154        out[1..].copy_from_slice(&self.bytes);
155        out
156    }
157
158    pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
159        if bytes.len() != Self::STORED_SIZE_USIZE {
160            return Err("corrupted EntityName: invalid size");
161        }
162
163        let len = bytes[0] as usize;
164        if len == 0 || len > MAX_ENTITY_NAME_LEN {
165            return Err("corrupted EntityName: invalid length");
166        }
167        if !bytes[1..=len].is_ascii() {
168            return Err("corrupted EntityName: invalid encoding");
169        }
170        if bytes[1 + len..].iter().any(|&b| b != 0) {
171            return Err("corrupted EntityName: non-zero padding");
172        }
173
174        let mut name = [0u8; MAX_ENTITY_NAME_LEN];
175        name.copy_from_slice(&bytes[1..]);
176
177        Ok(Self {
178            len: len as u8,
179            bytes: name,
180        })
181    }
182
183    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
184        Self::from_bytes(bytes)
185    }
186
187    #[must_use]
188    pub const fn max_storable() -> Self {
189        Self {
190            len: MAX_ENTITY_NAME_LEN as u8,
191            bytes: [b'z'; MAX_ENTITY_NAME_LEN],
192        }
193    }
194}
195
196impl TryFrom<&[u8]> for EntityName {
197    type Error = &'static str;
198
199    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
200        Self::try_from_bytes(bytes)
201    }
202}
203
204impl Ord for EntityName {
205    fn cmp(&self, other: &Self) -> Ordering {
206        self.len
207            .cmp(&other.len)
208            .then_with(|| self.bytes.cmp(&other.bytes))
209    }
210}
211
212impl PartialOrd for EntityName {
213    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
214        Some(self.cmp(other))
215    }
216}
217
218impl Display for EntityName {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        f.write_str(self.as_str())
221    }
222}
223
224impl fmt::Debug for EntityName {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        write!(f, "EntityName({})", self.as_str())
227    }
228}
229
230///
231/// IndexName
232///
233
234#[derive(Clone, Copy, Eq, Hash, PartialEq)]
235pub struct IndexName {
236    pub len: u16,
237    pub bytes: [u8; MAX_INDEX_NAME_LEN],
238}
239
240impl IndexName {
241    pub const STORED_SIZE: u32 = 2 + MAX_INDEX_NAME_LEN as u32;
242    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
243
244    #[must_use]
245    /// Build an index name from validated static identifiers.
246    ///
247    /// Caller must uphold all identity invariants; intended for generated code.
248    pub(crate) fn from_parts_unchecked(entity: &EntityName, fields: &[&str]) -> Self {
249        debug_assert!(
250            fields.len() <= MAX_INDEX_FIELDS,
251            "index has too many fields"
252        );
253
254        let mut out = [0u8; MAX_INDEX_NAME_LEN];
255        let mut len = 0usize;
256
257        Self::push_ascii(&mut out, &mut len, entity.as_bytes());
258
259        for field in fields {
260            debug_assert!(
261                field.len() <= MAX_INDEX_FIELD_NAME_LEN,
262                "index field name too long"
263            );
264            Self::push_ascii(&mut out, &mut len, b"|");
265            Self::push_ascii(&mut out, &mut len, field.as_bytes());
266        }
267
268        Self {
269            len: len as u16,
270            bytes: out,
271        }
272    }
273
274    /// Build an index name from runtime input, returning a typed error on failure.
275    pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
276        // Phase 1: validate field limits and total name length.
277        if fields.len() > MAX_INDEX_FIELDS {
278            return Err(IndexNameError::TooManyFields {
279                len: fields.len(),
280                max: MAX_INDEX_FIELDS,
281            });
282        }
283
284        let mut total_len = entity.len();
285        for field in fields {
286            let field_len = field.len();
287            if field_len > MAX_INDEX_FIELD_NAME_LEN {
288                return Err(IndexNameError::FieldTooLong {
289                    field: (*field).to_string(),
290                    max: MAX_INDEX_FIELD_NAME_LEN,
291                });
292            }
293            if !field.is_ascii() {
294                return Err(IndexNameError::FieldNonAscii {
295                    field: (*field).to_string(),
296                });
297            }
298            total_len = total_len.saturating_add(1 + field_len);
299        }
300
301        if total_len > MAX_INDEX_NAME_LEN {
302            return Err(IndexNameError::TooLong {
303                len: total_len,
304                max: MAX_INDEX_NAME_LEN,
305            });
306        }
307
308        // Phase 2: build the fixed-size byte representation.
309        let mut out = [0u8; MAX_INDEX_NAME_LEN];
310        let mut len = 0usize;
311
312        Self::push_bytes(&mut out, &mut len, entity.as_bytes());
313        for field in fields {
314            Self::push_bytes(&mut out, &mut len, b"|");
315            Self::push_bytes(&mut out, &mut len, field.as_bytes());
316        }
317
318        Ok(Self {
319            len: len as u16,
320            bytes: out,
321        })
322    }
323
324    #[must_use]
325    pub fn as_bytes(&self) -> &[u8] {
326        &self.bytes[..self.len as usize]
327    }
328
329    #[must_use]
330    pub fn as_str(&self) -> &str {
331        unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
332    }
333
334    #[must_use]
335    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
336        let mut out = [0u8; Self::STORED_SIZE_USIZE];
337        out[..2].copy_from_slice(&self.len.to_be_bytes());
338        out[2..].copy_from_slice(&self.bytes);
339        out
340    }
341
342    pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
343        if bytes.len() != Self::STORED_SIZE_USIZE {
344            return Err("corrupted IndexName: invalid size");
345        }
346
347        let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
348        if len == 0 || len > MAX_INDEX_NAME_LEN {
349            return Err("corrupted IndexName: invalid length");
350        }
351        if !bytes[2..2 + len].is_ascii() {
352            return Err("corrupted IndexName: invalid encoding");
353        }
354        if bytes[2 + len..].iter().any(|&b| b != 0) {
355            return Err("corrupted IndexName: non-zero padding");
356        }
357
358        let mut name = [0u8; MAX_INDEX_NAME_LEN];
359        name.copy_from_slice(&bytes[2..]);
360
361        Ok(Self {
362            len: len as u16,
363            bytes: name,
364        })
365    }
366
367    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
368        Self::from_bytes(bytes)
369    }
370
371    fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
372        let end = *len + bytes.len();
373        out[*len..end].copy_from_slice(bytes);
374        *len = end;
375    }
376
377    fn push_ascii(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
378        debug_assert!(bytes.is_ascii(), "index name must be ASCII");
379        debug_assert!(
380            *len + bytes.len() <= MAX_INDEX_NAME_LEN,
381            "index name too long"
382        );
383
384        out[*len..*len + bytes.len()].copy_from_slice(bytes);
385        *len += bytes.len();
386    }
387
388    #[must_use]
389    pub const fn max_storable() -> Self {
390        Self {
391            len: MAX_INDEX_NAME_LEN as u16,
392            bytes: [b'z'; MAX_INDEX_NAME_LEN],
393        }
394    }
395}
396
397impl TryFrom<&[u8]> for IndexName {
398    type Error = &'static str;
399
400    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
401        Self::try_from_bytes(bytes)
402    }
403}
404
405impl fmt::Debug for IndexName {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        write!(f, "IndexName({})", self.as_str())
408    }
409}
410
411impl Display for IndexName {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        write!(f, "{}", self.as_str())
414    }
415}
416
417impl Ord for IndexName {
418    fn cmp(&self, other: &Self) -> Ordering {
419        self.len
420            .cmp(&other.len)
421            .then_with(|| self.bytes.cmp(&other.bytes))
422    }
423}
424
425impl PartialOrd for IndexName {
426    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
427        Some(self.cmp(other))
428    }
429}
430
431///
432/// TESTS
433///
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    const ENTITY_64: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
440    const ENTITY_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
441    const FIELD_64_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
442    const FIELD_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
443    const FIELD_64_C: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
444    const FIELD_64_D: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
445
446    #[test]
447    fn index_name_max_len_matches_limits() {
448        let entity = EntityName::from_static_unchecked(ENTITY_64);
449        let fields = [FIELD_64_A, FIELD_64_B, FIELD_64_C, FIELD_64_D];
450
451        assert_eq!(entity.as_str().len(), MAX_ENTITY_NAME_LEN);
452        for field in &fields {
453            assert_eq!(field.len(), MAX_INDEX_FIELD_NAME_LEN);
454        }
455        assert_eq!(fields.len(), MAX_INDEX_FIELDS);
456
457        let name = IndexName::from_parts_unchecked(&entity, &fields);
458
459        assert_eq!(name.as_bytes().len(), MAX_INDEX_NAME_LEN);
460    }
461
462    #[test]
463    fn index_name_max_size_roundtrip_and_ordering() {
464        let entity_a = EntityName::from_static_unchecked(ENTITY_64);
465        let entity_b = EntityName::from_static_unchecked(ENTITY_64_B);
466        let fields_a = [FIELD_64_A, FIELD_64_A, FIELD_64_A, FIELD_64_A];
467        let fields_b = [FIELD_64_B, FIELD_64_B, FIELD_64_B, FIELD_64_B];
468
469        let idx_a = IndexName::from_parts_unchecked(&entity_a, &fields_a);
470        let idx_b = IndexName::from_parts_unchecked(&entity_b, &fields_b);
471
472        assert_eq!(idx_a.as_bytes().len(), MAX_INDEX_NAME_LEN);
473        assert_eq!(idx_b.as_bytes().len(), MAX_INDEX_NAME_LEN);
474
475        let decoded = IndexName::from_bytes(&idx_a.to_bytes()).unwrap();
476        assert_eq!(idx_a, decoded);
477
478        assert_eq!(idx_a.cmp(&idx_b), idx_a.to_bytes().cmp(&idx_b.to_bytes()));
479    }
480
481    #[test]
482    fn rejects_too_many_index_fields() {
483        let entity = EntityName::try_from_str("entity").expect("entity name");
484        let fields = ["a", "b", "c", "d", "e"];
485        let err = IndexName::try_from_parts(&entity, &fields).unwrap_err();
486        assert!(matches!(err, IndexNameError::TooManyFields { .. }));
487    }
488
489    #[test]
490    fn rejects_index_field_over_len() {
491        let entity = EntityName::try_from_str("entity").expect("entity name");
492        let long_field = "a".repeat(MAX_INDEX_FIELD_NAME_LEN + 1);
493        let fields = [long_field.as_str()];
494        let err = IndexName::try_from_parts(&entity, &fields).unwrap_err();
495        assert!(matches!(err, IndexNameError::FieldTooLong { .. }));
496    }
497
498    #[test]
499    fn entity_from_static_roundtrip() {
500        let e = EntityName::try_from_static("user").expect("entity name");
501        assert_eq!(e.len(), 4);
502        assert_eq!(e.as_str(), "user");
503    }
504
505    #[test]
506    fn entity_try_from_static_rejects_empty() {
507        let err = EntityName::try_from_static("").unwrap_err();
508        assert!(matches!(err, EntityNameError::Empty));
509    }
510
511    #[test]
512    fn entity_try_from_static_rejects_len_over_max() {
513        let s = "a".repeat(MAX_ENTITY_NAME_LEN + 1);
514        let leaked = Box::leak(s.into_boxed_str());
515        let err = EntityName::try_from_static(leaked).unwrap_err();
516        assert!(matches!(err, EntityNameError::TooLong { .. }));
517    }
518
519    #[test]
520    fn entity_rejects_empty() {
521        let err = EntityName::try_from_str("").unwrap_err();
522        assert!(matches!(err, EntityNameError::Empty));
523    }
524
525    #[test]
526    fn entity_rejects_non_ascii() {
527        let err = EntityName::try_from_str("usér").unwrap_err();
528        assert!(matches!(err, EntityNameError::NonAscii));
529    }
530
531    #[test]
532    fn entity_storage_roundtrip() {
533        let e = EntityName::try_from_static("entity_name").expect("entity name");
534        let bytes = e.to_bytes();
535        let decoded = EntityName::from_bytes(&bytes).unwrap();
536        assert_eq!(e, decoded);
537    }
538
539    #[test]
540    fn entity_rejects_invalid_size() {
541        let buf = vec![0u8; EntityName::STORED_SIZE_USIZE - 1];
542        assert!(EntityName::from_bytes(&buf).is_err());
543    }
544
545    #[test]
546    fn entity_rejects_invalid_size_oversized() {
547        let buf = vec![0u8; EntityName::STORED_SIZE_USIZE + 1];
548        assert!(EntityName::from_bytes(&buf).is_err());
549    }
550
551    #[test]
552    fn entity_rejects_len_over_max() {
553        let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
554        buf[0] = (MAX_ENTITY_NAME_LEN as u8).saturating_add(1);
555        assert!(EntityName::from_bytes(&buf).is_err());
556    }
557
558    #[test]
559    fn entity_rejects_non_ascii_from_bytes() {
560        let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
561        buf[0] = 1;
562        buf[1] = 0xFF;
563        assert!(EntityName::from_bytes(&buf).is_err());
564    }
565
566    #[test]
567    fn entity_rejects_non_zero_padding() {
568        let e = EntityName::from_static_unchecked("user");
569        let mut bytes = e.to_bytes();
570        bytes[1 + e.len()] = b'x';
571        assert!(EntityName::from_bytes(&bytes).is_err());
572    }
573
574    #[test]
575    fn entity_ordering_matches_bytes() {
576        let a = EntityName::from_static_unchecked("abc");
577        let b = EntityName::from_static_unchecked("abd");
578        let c = EntityName::from_static_unchecked("abcx");
579
580        assert_eq!(a.cmp(&b), a.to_bytes().cmp(&b.to_bytes()));
581        assert_eq!(a.cmp(&c), a.to_bytes().cmp(&c.to_bytes()));
582    }
583
584    #[test]
585    fn entity_ordering_b_vs_aa() {
586        let b = EntityName::from_static_unchecked("b");
587        let aa = EntityName::from_static_unchecked("aa");
588        assert_eq!(b.cmp(&aa), b.to_bytes().cmp(&aa.to_bytes()));
589    }
590
591    #[test]
592    fn entity_ordering_prefix_matches_bytes() {
593        let a = EntityName::from_static_unchecked("a");
594        let aa = EntityName::from_static_unchecked("aa");
595        assert_eq!(a.cmp(&aa), a.to_bytes().cmp(&aa.to_bytes()));
596    }
597
598    #[test]
599    fn index_single_field_format() {
600        let entity = EntityName::from_static_unchecked("user");
601        let idx = IndexName::from_parts_unchecked(&entity, &["email"]);
602
603        assert_eq!(idx.as_str(), "user|email");
604    }
605
606    #[test]
607    fn index_field_order_is_preserved() {
608        let entity = EntityName::from_static_unchecked("user");
609        let idx = IndexName::from_parts_unchecked(&entity, &["a", "b", "c"]);
610
611        assert_eq!(idx.as_str(), "user|a|b|c");
612    }
613
614    #[test]
615    fn index_storage_roundtrip() {
616        let entity = EntityName::from_static_unchecked("user");
617        let idx = IndexName::from_parts_unchecked(&entity, &["a", "b"]);
618
619        let bytes = idx.to_bytes();
620        let decoded = IndexName::from_bytes(&bytes).unwrap();
621
622        assert_eq!(idx, decoded);
623    }
624
625    #[test]
626    fn index_rejects_zero_len() {
627        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
628        buf[0] = 0;
629        assert!(IndexName::from_bytes(&buf).is_err());
630    }
631
632    #[test]
633    fn index_rejects_invalid_size_oversized() {
634        let buf = vec![0u8; IndexName::STORED_SIZE_USIZE + 1];
635        assert!(IndexName::from_bytes(&buf).is_err());
636    }
637
638    #[test]
639    fn index_rejects_len_over_max() {
640        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
641        let len = (MAX_INDEX_NAME_LEN as u16).saturating_add(1);
642        buf[..2].copy_from_slice(&len.to_be_bytes());
643        assert!(IndexName::from_bytes(&buf).is_err());
644    }
645
646    #[test]
647    fn index_rejects_non_ascii_from_bytes() {
648        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
649        buf[..2].copy_from_slice(&1u16.to_be_bytes());
650        buf[2] = 0xFF;
651        assert!(IndexName::from_bytes(&buf).is_err());
652    }
653
654    #[test]
655    fn index_rejects_non_zero_padding() {
656        let entity = EntityName::from_static_unchecked("user");
657        let idx = IndexName::from_parts_unchecked(&entity, &["a"]);
658        let mut bytes = idx.to_bytes();
659        bytes[2 + idx.len as usize] = b'x';
660        assert!(IndexName::from_bytes(&bytes).is_err());
661    }
662
663    #[test]
664    fn index_ordering_matches_bytes() {
665        let entity = EntityName::from_static_unchecked("user");
666
667        let a = IndexName::from_parts_unchecked(&entity, &["a"]);
668        let ab = IndexName::from_parts_unchecked(&entity, &["a", "b"]);
669        let b = IndexName::from_parts_unchecked(&entity, &["b"]);
670
671        assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
672        assert_eq!(ab.cmp(&b), ab.to_bytes().cmp(&b.to_bytes()));
673    }
674
675    #[test]
676    fn index_ordering_prefix_matches_bytes() {
677        let entity = EntityName::from_static_unchecked("user");
678        let a = IndexName::from_parts_unchecked(&entity, &["a"]);
679        let ab = IndexName::from_parts_unchecked(&entity, &["a", "b"]);
680        assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
681    }
682
683    #[test]
684    fn max_storable_orders_last() {
685        let entity = EntityName::from_static_unchecked("zz");
686        let max = EntityName::max_storable();
687
688        assert!(entity < max);
689    }
690
691    ///
692    /// FUZZING
693    ///
694
695    /// Simple deterministic ASCII generator
696    fn gen_ascii(seed: u64, max_len: usize) -> String {
697        let len = (seed as usize % max_len).max(1);
698        let mut out = String::with_capacity(len);
699
700        let mut x = seed;
701        for _ in 0..len {
702            // printable ASCII range [a–z]
703            x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
704            let c = b'a' + (x % 26) as u8;
705            out.push(c as char);
706        }
707
708        out
709    }
710
711    #[test]
712    fn fuzz_entity_name_roundtrip_and_ordering() {
713        const RUNS: u64 = 1_000;
714
715        let mut prev: Option<EntityName> = None;
716
717        for i in 1..=RUNS {
718            let s = gen_ascii(i, MAX_ENTITY_NAME_LEN);
719            let e = EntityName::from_static_unchecked(Box::leak(s.clone().into_boxed_str()));
720
721            // Round-trip
722            let bytes = e.to_bytes();
723            let decoded = EntityName::from_bytes(&bytes).unwrap();
724            assert_eq!(e, decoded);
725
726            // Ordering vs bytes
727            if let Some(p) = prev {
728                let ord_entity = p.cmp(&e);
729                let ord_bytes = p.to_bytes().cmp(&e.to_bytes());
730                assert_eq!(ord_entity, ord_bytes);
731            }
732
733            prev = Some(e);
734        }
735    }
736
737    #[test]
738    fn fuzz_index_name_roundtrip_and_ordering() {
739        const RUNS: u64 = 1_000;
740
741        let entity = EntityName::from_static_unchecked("entity");
742        let mut prev: Option<IndexName> = None;
743
744        for i in 1..=RUNS {
745            let field_count = (i as usize % MAX_INDEX_FIELDS).max(1);
746
747            let mut field_strings = Vec::with_capacity(field_count);
748            let mut fields = Vec::with_capacity(field_count);
749            let mut string_parts = Vec::with_capacity(field_count + 1);
750
751            string_parts.push(entity.as_str().to_owned());
752
753            for f in 0..field_count {
754                let s = gen_ascii(i * 31 + f as u64, MAX_INDEX_FIELD_NAME_LEN);
755                string_parts.push(s.clone());
756                field_strings.push(s);
757            }
758
759            for s in &field_strings {
760                fields.push(s.as_str());
761            }
762
763            let idx = IndexName::from_parts_unchecked(&entity, &fields);
764            let expected = string_parts.join("|");
765
766            // Structural correctness
767            assert_eq!(idx.as_str(), expected);
768
769            // Round-trip
770            let bytes = idx.to_bytes();
771            let decoded = IndexName::from_bytes(&bytes).unwrap();
772            assert_eq!(idx, decoded);
773
774            // Ordering vs bytes
775            if let Some(p_idx) = prev {
776                let ord_idx = p_idx.cmp(&idx);
777                let ord_bytes = p_idx.to_bytes().cmp(&idx.to_bytes());
778                assert_eq!(ord_idx, ord_bytes);
779            }
780
781            prev = Some(idx);
782        }
783    }
784}