icydb_core/db/store/
identity.rs

1#![allow(clippy::cast_possible_truncation)]
2
3use crate::MAX_INDEX_FIELDS;
4use std::{
5    cmp::Ordering,
6    fmt::{self, Display},
7};
8
9///
10/// Constants
11///
12
13pub const MAX_ENTITY_NAME_LEN: usize = 48;
14pub const MAX_INDEX_FIELD_NAME_LEN: usize = 48;
15pub const MAX_INDEX_NAME_LEN: usize =
16    MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
17
18///
19/// EntityName
20///
21
22#[derive(Clone, Copy, Eq, Hash, PartialEq)]
23pub struct EntityName {
24    pub len: u8,
25    pub bytes: [u8; MAX_ENTITY_NAME_LEN],
26}
27
28impl EntityName {
29    pub const STORED_SIZE: u32 = 1 + MAX_ENTITY_NAME_LEN as u32;
30    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
31
32    #[must_use]
33    pub const fn from_static(name: &'static str) -> Self {
34        let bytes = name.as_bytes();
35        let len = bytes.len();
36
37        assert!(
38            len > 0 && len <= MAX_ENTITY_NAME_LEN,
39            "entity name length out of bounds"
40        );
41
42        let mut out = [0u8; MAX_ENTITY_NAME_LEN];
43        let mut i = 0;
44        while i < len {
45            let b = bytes[i];
46            assert!(b.is_ascii(), "entity name must be ASCII");
47            out[i] = b;
48            i += 1;
49        }
50
51        Self {
52            len: len as u8,
53            bytes: out,
54        }
55    }
56
57    #[must_use]
58    pub const fn len(&self) -> usize {
59        self.len as usize
60    }
61
62    #[must_use]
63    pub const fn is_empty(&self) -> bool {
64        self.len == 0
65    }
66
67    #[must_use]
68    pub fn as_bytes(&self) -> &[u8] {
69        &self.bytes[..self.len()]
70    }
71
72    #[must_use]
73    pub fn as_str(&self) -> &str {
74        // Safe because we enforce ASCII
75        unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
76    }
77
78    #[must_use]
79    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
80        let mut out = [0u8; Self::STORED_SIZE_USIZE];
81        out[0] = self.len;
82        out[1..].copy_from_slice(&self.bytes);
83        out
84    }
85
86    pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
87        if bytes.len() != Self::STORED_SIZE_USIZE {
88            return Err("invalid EntityName size");
89        }
90
91        let len = bytes[0] as usize;
92        if len == 0 || len > MAX_ENTITY_NAME_LEN {
93            return Err("invalid EntityName length");
94        }
95        if !bytes[1..=len].is_ascii() {
96            return Err("invalid EntityName encoding");
97        }
98        if bytes[1 + len..].iter().any(|&b| b != 0) {
99            return Err("invalid EntityName padding");
100        }
101
102        let mut name = [0u8; MAX_ENTITY_NAME_LEN];
103        name.copy_from_slice(&bytes[1..]);
104
105        Ok(Self {
106            len: len as u8,
107            bytes: name,
108        })
109    }
110
111    #[must_use]
112    pub const fn max_storable() -> Self {
113        Self {
114            len: MAX_ENTITY_NAME_LEN as u8,
115            bytes: [b'z'; MAX_ENTITY_NAME_LEN],
116        }
117    }
118}
119
120impl Ord for EntityName {
121    fn cmp(&self, other: &Self) -> Ordering {
122        self.len
123            .cmp(&other.len)
124            .then_with(|| self.bytes.cmp(&other.bytes))
125    }
126}
127
128impl PartialOrd for EntityName {
129    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
130        Some(self.cmp(other))
131    }
132}
133
134impl Display for EntityName {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        f.write_str(self.as_str())
137    }
138}
139
140impl fmt::Debug for EntityName {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(f, "EntityName({})", self.as_str())
143    }
144}
145
146///
147/// IndexName
148///
149
150#[derive(Clone, Copy, Eq, Hash, PartialEq)]
151pub struct IndexName {
152    pub len: u8,
153    pub bytes: [u8; MAX_INDEX_NAME_LEN],
154}
155
156impl IndexName {
157    pub const STORED_SIZE: u32 = 1 + MAX_INDEX_NAME_LEN as u32;
158    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
159
160    #[must_use]
161    pub fn from_parts(entity: &EntityName, fields: &[&str]) -> Self {
162        assert!(
163            fields.len() <= MAX_INDEX_FIELDS,
164            "index has too many fields"
165        );
166
167        let mut out = [0u8; MAX_INDEX_NAME_LEN];
168        let mut len = 0usize;
169
170        Self::push_ascii(&mut out, &mut len, entity.as_bytes());
171
172        for field in fields {
173            assert!(
174                field.len() <= MAX_INDEX_FIELD_NAME_LEN,
175                "index field name too long"
176            );
177            Self::push_ascii(&mut out, &mut len, b"|");
178            Self::push_ascii(&mut out, &mut len, field.as_bytes());
179        }
180
181        Self {
182            len: len as u8,
183            bytes: out,
184        }
185    }
186
187    #[must_use]
188    pub fn as_bytes(&self) -> &[u8] {
189        &self.bytes[..self.len as usize]
190    }
191
192    #[must_use]
193    pub fn as_str(&self) -> &str {
194        unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
195    }
196
197    #[must_use]
198    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
199        let mut out = [0u8; Self::STORED_SIZE_USIZE];
200        out[0] = self.len;
201        out[1..].copy_from_slice(&self.bytes);
202        out
203    }
204
205    pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
206        if bytes.len() != Self::STORED_SIZE_USIZE {
207            return Err("invalid IndexName size");
208        }
209
210        let len = bytes[0] as usize;
211        if len == 0 || len > MAX_INDEX_NAME_LEN {
212            return Err("invalid IndexName length");
213        }
214        if !bytes[1..=len].is_ascii() {
215            return Err("invalid IndexName encoding");
216        }
217        if bytes[1 + len..].iter().any(|&b| b != 0) {
218            return Err("invalid IndexName padding");
219        }
220
221        let mut name = [0u8; MAX_INDEX_NAME_LEN];
222        name.copy_from_slice(&bytes[1..]);
223
224        Ok(Self {
225            len: len as u8,
226            bytes: name,
227        })
228    }
229
230    fn push_ascii(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
231        assert!(bytes.is_ascii(), "index name must be ASCII");
232        assert!(
233            *len + bytes.len() <= MAX_INDEX_NAME_LEN,
234            "index name too long"
235        );
236
237        out[*len..*len + bytes.len()].copy_from_slice(bytes);
238        *len += bytes.len();
239    }
240
241    #[must_use]
242    pub const fn max_storable() -> Self {
243        Self {
244            len: MAX_INDEX_NAME_LEN as u8,
245            bytes: [b'z'; MAX_INDEX_NAME_LEN],
246        }
247    }
248}
249
250impl fmt::Debug for IndexName {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        write!(f, "IndexName({})", self.as_str())
253    }
254}
255
256impl Display for IndexName {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        write!(f, "{}", self.as_str())
259    }
260}
261
262impl Ord for IndexName {
263    fn cmp(&self, other: &Self) -> Ordering {
264        self.len
265            .cmp(&other.len)
266            .then_with(|| self.bytes.cmp(&other.bytes))
267    }
268}
269
270impl PartialOrd for IndexName {
271    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
272        Some(self.cmp(other))
273    }
274}
275
276///
277/// TESTS
278///
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    const ENTITY_48: &str = "0123456789abcdef0123456789abcdef0123456789abcdef";
285    const FIELD_48_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
286    const FIELD_48_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
287    const FIELD_48_C: &str = "cccccccccccccccccccccccccccccccccccccccccccccccc";
288    const FIELD_48_D: &str = "dddddddddddddddddddddddddddddddddddddddddddddddd";
289
290    #[test]
291    fn index_name_max_len_matches_limits() {
292        let entity = EntityName::from_static(ENTITY_48);
293        let fields = [FIELD_48_A, FIELD_48_B, FIELD_48_C, FIELD_48_D];
294
295        assert_eq!(entity.as_str().len(), MAX_ENTITY_NAME_LEN);
296        for field in &fields {
297            assert_eq!(field.len(), MAX_INDEX_FIELD_NAME_LEN);
298        }
299        assert_eq!(fields.len(), MAX_INDEX_FIELDS);
300
301        let name = IndexName::from_parts(&entity, &fields);
302
303        assert_eq!(name.as_bytes().len(), MAX_INDEX_NAME_LEN);
304    }
305
306    #[test]
307    #[should_panic(expected = "index has too many fields")]
308    fn rejects_too_many_index_fields() {
309        let entity = EntityName::from_static("entity");
310        let fields = ["a", "b", "c", "d", "e"];
311        let _ = IndexName::from_parts(&entity, &fields);
312    }
313
314    #[test]
315    #[should_panic(expected = "index field name too long")]
316    fn rejects_index_field_over_len() {
317        let entity = EntityName::from_static("entity");
318        let long_field = "a".repeat(MAX_INDEX_FIELD_NAME_LEN + 1);
319        let fields = [long_field.as_str()];
320        let _ = IndexName::from_parts(&entity, &fields);
321    }
322
323    #[test]
324    fn entity_from_static_roundtrip() {
325        let e = EntityName::from_static("user");
326        assert_eq!(e.len(), 4);
327        assert_eq!(e.as_str(), "user");
328    }
329
330    #[test]
331    #[should_panic(expected = "entity name length out of bounds")]
332    fn entity_rejects_empty() {
333        let _ = EntityName::from_static("");
334    }
335
336    #[test]
337    #[should_panic(expected = "entity name must be ASCII")]
338    fn entity_rejects_non_ascii() {
339        let _ = EntityName::from_static("usér");
340    }
341
342    #[test]
343    fn entity_storage_roundtrip() {
344        let e = EntityName::from_static("entity_name");
345        let bytes = e.to_bytes();
346        let decoded = EntityName::from_bytes(&bytes).unwrap();
347        assert_eq!(e, decoded);
348    }
349
350    #[test]
351    fn entity_rejects_invalid_size() {
352        let buf = vec![0u8; EntityName::STORED_SIZE_USIZE - 1];
353        assert!(EntityName::from_bytes(&buf).is_err());
354    }
355
356    #[test]
357    fn entity_rejects_invalid_size_oversized() {
358        let buf = vec![0u8; EntityName::STORED_SIZE_USIZE + 1];
359        assert!(EntityName::from_bytes(&buf).is_err());
360    }
361
362    #[test]
363    fn entity_rejects_len_over_max() {
364        let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
365        buf[0] = (MAX_ENTITY_NAME_LEN as u8).saturating_add(1);
366        assert!(EntityName::from_bytes(&buf).is_err());
367    }
368
369    #[test]
370    fn entity_rejects_non_ascii_from_bytes() {
371        let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
372        buf[0] = 1;
373        buf[1] = 0xFF;
374        assert!(EntityName::from_bytes(&buf).is_err());
375    }
376
377    #[test]
378    fn entity_rejects_non_zero_padding() {
379        let e = EntityName::from_static("user");
380        let mut bytes = e.to_bytes();
381        bytes[1 + e.len()] = b'x';
382        assert!(EntityName::from_bytes(&bytes).is_err());
383    }
384
385    #[test]
386    fn entity_ordering_matches_bytes() {
387        let a = EntityName::from_static("abc");
388        let b = EntityName::from_static("abd");
389        let c = EntityName::from_static("abcx");
390
391        assert_eq!(a.cmp(&b), a.to_bytes().cmp(&b.to_bytes()));
392        assert_eq!(a.cmp(&c), a.to_bytes().cmp(&c.to_bytes()));
393    }
394
395    #[test]
396    fn entity_ordering_b_vs_aa() {
397        let b = EntityName::from_static("b");
398        let aa = EntityName::from_static("aa");
399        assert_eq!(b.cmp(&aa), b.to_bytes().cmp(&aa.to_bytes()));
400    }
401
402    #[test]
403    fn entity_ordering_prefix_matches_bytes() {
404        let a = EntityName::from_static("a");
405        let aa = EntityName::from_static("aa");
406        assert_eq!(a.cmp(&aa), a.to_bytes().cmp(&aa.to_bytes()));
407    }
408
409    #[test]
410    fn index_single_field_format() {
411        let entity = EntityName::from_static("user");
412        let idx = IndexName::from_parts(&entity, &["email"]);
413
414        assert_eq!(idx.as_str(), "user|email");
415    }
416
417    #[test]
418    fn index_field_order_is_preserved() {
419        let entity = EntityName::from_static("user");
420        let idx = IndexName::from_parts(&entity, &["a", "b", "c"]);
421
422        assert_eq!(idx.as_str(), "user|a|b|c");
423    }
424
425    #[test]
426    fn index_storage_roundtrip() {
427        let entity = EntityName::from_static("user");
428        let idx = IndexName::from_parts(&entity, &["a", "b"]);
429
430        let bytes = idx.to_bytes();
431        let decoded = IndexName::from_bytes(&bytes).unwrap();
432
433        assert_eq!(idx, decoded);
434    }
435
436    #[test]
437    fn index_rejects_zero_len() {
438        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
439        buf[0] = 0;
440        assert!(IndexName::from_bytes(&buf).is_err());
441    }
442
443    #[test]
444    fn index_rejects_invalid_size_oversized() {
445        let buf = vec![0u8; IndexName::STORED_SIZE_USIZE + 1];
446        assert!(IndexName::from_bytes(&buf).is_err());
447    }
448
449    #[test]
450    fn index_rejects_len_over_max() {
451        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
452        buf[0] = (MAX_INDEX_NAME_LEN as u8).saturating_add(1);
453        assert!(IndexName::from_bytes(&buf).is_err());
454    }
455
456    #[test]
457    fn index_rejects_non_ascii_from_bytes() {
458        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
459        buf[0] = 1;
460        buf[1] = 0xFF;
461        assert!(IndexName::from_bytes(&buf).is_err());
462    }
463
464    #[test]
465    fn index_rejects_non_zero_padding() {
466        let entity = EntityName::from_static("user");
467        let idx = IndexName::from_parts(&entity, &["a"]);
468        let mut bytes = idx.to_bytes();
469        bytes[1 + idx.len as usize] = b'x';
470        assert!(IndexName::from_bytes(&bytes).is_err());
471    }
472
473    #[test]
474    fn index_ordering_matches_bytes() {
475        let entity = EntityName::from_static("user");
476
477        let a = IndexName::from_parts(&entity, &["a"]);
478        let ab = IndexName::from_parts(&entity, &["a", "b"]);
479        let b = IndexName::from_parts(&entity, &["b"]);
480
481        assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
482        assert_eq!(ab.cmp(&b), ab.to_bytes().cmp(&b.to_bytes()));
483    }
484
485    #[test]
486    fn index_ordering_prefix_matches_bytes() {
487        let entity = EntityName::from_static("user");
488        let a = IndexName::from_parts(&entity, &["a"]);
489        let ab = IndexName::from_parts(&entity, &["a", "b"]);
490        assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
491    }
492
493    #[test]
494    fn max_storable_orders_last() {
495        let entity = EntityName::from_static("zz");
496        let max = EntityName::max_storable();
497
498        assert!(entity < max);
499    }
500
501    ///
502    /// FUZZING
503    ///
504
505    /// Simple deterministic ASCII generator
506    fn gen_ascii(seed: u64, max_len: usize) -> String {
507        let len = (seed as usize % max_len).max(1);
508        let mut out = String::with_capacity(len);
509
510        let mut x = seed;
511        for _ in 0..len {
512            // printable ASCII range [a–z]
513            x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
514            let c = b'a' + (x % 26) as u8;
515            out.push(c as char);
516        }
517
518        out
519    }
520
521    #[test]
522    fn fuzz_entity_name_roundtrip_and_ordering() {
523        const RUNS: u64 = 1_000;
524
525        let mut prev: Option<EntityName> = None;
526
527        for i in 1..=RUNS {
528            let s = gen_ascii(i, MAX_ENTITY_NAME_LEN);
529            let e = EntityName::from_static(Box::leak(s.clone().into_boxed_str()));
530
531            // Round-trip
532            let bytes = e.to_bytes();
533            let decoded = EntityName::from_bytes(&bytes).unwrap();
534            assert_eq!(e, decoded);
535
536            // Ordering vs bytes
537            if let Some(p) = prev {
538                let ord_entity = p.cmp(&e);
539                let ord_bytes = p.to_bytes().cmp(&e.to_bytes());
540                assert_eq!(ord_entity, ord_bytes);
541            }
542
543            prev = Some(e);
544        }
545    }
546
547    #[test]
548    fn fuzz_index_name_roundtrip_and_ordering() {
549        const RUNS: u64 = 1_000;
550
551        let entity = EntityName::from_static("entity");
552        let mut prev: Option<IndexName> = None;
553
554        for i in 1..=RUNS {
555            let field_count = (i as usize % MAX_INDEX_FIELDS).max(1);
556
557            let mut field_strings = Vec::with_capacity(field_count);
558            let mut fields = Vec::with_capacity(field_count);
559            let mut string_parts = Vec::with_capacity(field_count + 1);
560
561            string_parts.push(entity.as_str().to_owned());
562
563            for f in 0..field_count {
564                let s = gen_ascii(i * 31 + f as u64, MAX_INDEX_FIELD_NAME_LEN);
565                string_parts.push(s.clone());
566                field_strings.push(s);
567            }
568
569            for s in &field_strings {
570                fields.push(s.as_str());
571            }
572
573            let idx = IndexName::from_parts(&entity, &fields);
574            let expected = string_parts.join("|");
575
576            // Structural correctness
577            assert_eq!(idx.as_str(), expected);
578
579            // Round-trip
580            let bytes = idx.to_bytes();
581            let decoded = IndexName::from_bytes(&bytes).unwrap();
582            assert_eq!(idx, decoded);
583
584            // Ordering vs bytes
585            if let Some(p_idx) = prev {
586                let ord_idx = p_idx.cmp(&idx);
587                let ord_bytes = p_idx.to_bytes().cmp(&idx.to_bytes());
588                assert_eq!(ord_idx, ord_bytes);
589            }
590
591            prev = Some(idx);
592        }
593    }
594}