icydb_core/db/
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 = 64;
14pub const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
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("corrupted EntityName: invalid size");
89        }
90
91        let len = bytes[0] as usize;
92        if len == 0 || len > MAX_ENTITY_NAME_LEN {
93            return Err("corrupted EntityName: invalid length");
94        }
95        if !bytes[1..=len].is_ascii() {
96            return Err("corrupted EntityName: invalid encoding");
97        }
98        if bytes[1 + len..].iter().any(|&b| b != 0) {
99            return Err("corrupted EntityName: non-zero 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    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
112        Self::from_bytes(bytes)
113    }
114
115    #[must_use]
116    pub const fn max_storable() -> Self {
117        Self {
118            len: MAX_ENTITY_NAME_LEN as u8,
119            bytes: [b'z'; MAX_ENTITY_NAME_LEN],
120        }
121    }
122}
123
124impl TryFrom<&[u8]> for EntityName {
125    type Error = &'static str;
126
127    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
128        Self::try_from_bytes(bytes)
129    }
130}
131
132impl Ord for EntityName {
133    fn cmp(&self, other: &Self) -> Ordering {
134        self.len
135            .cmp(&other.len)
136            .then_with(|| self.bytes.cmp(&other.bytes))
137    }
138}
139
140impl PartialOrd for EntityName {
141    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
142        Some(self.cmp(other))
143    }
144}
145
146impl Display for EntityName {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.write_str(self.as_str())
149    }
150}
151
152impl fmt::Debug for EntityName {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "EntityName({})", self.as_str())
155    }
156}
157
158///
159/// IndexName
160///
161
162#[derive(Clone, Copy, Eq, Hash, PartialEq)]
163pub struct IndexName {
164    pub len: u16,
165    pub bytes: [u8; MAX_INDEX_NAME_LEN],
166}
167
168impl IndexName {
169    pub const STORED_SIZE: u32 = 2 + MAX_INDEX_NAME_LEN as u32;
170    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
171
172    #[must_use]
173    pub fn from_parts(entity: &EntityName, fields: &[&str]) -> Self {
174        assert!(
175            fields.len() <= MAX_INDEX_FIELDS,
176            "index has too many fields"
177        );
178
179        let mut out = [0u8; MAX_INDEX_NAME_LEN];
180        let mut len = 0usize;
181
182        Self::push_ascii(&mut out, &mut len, entity.as_bytes());
183
184        for field in fields {
185            assert!(
186                field.len() <= MAX_INDEX_FIELD_NAME_LEN,
187                "index field name too long"
188            );
189            Self::push_ascii(&mut out, &mut len, b"|");
190            Self::push_ascii(&mut out, &mut len, field.as_bytes());
191        }
192
193        Self {
194            len: len as u16,
195            bytes: out,
196        }
197    }
198
199    #[must_use]
200    pub fn as_bytes(&self) -> &[u8] {
201        &self.bytes[..self.len as usize]
202    }
203
204    #[must_use]
205    pub fn as_str(&self) -> &str {
206        unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
207    }
208
209    #[must_use]
210    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
211        let mut out = [0u8; Self::STORED_SIZE_USIZE];
212        out[..2].copy_from_slice(&self.len.to_be_bytes());
213        out[2..].copy_from_slice(&self.bytes);
214        out
215    }
216
217    pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
218        if bytes.len() != Self::STORED_SIZE_USIZE {
219            return Err("corrupted IndexName: invalid size");
220        }
221
222        let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
223        if len == 0 || len > MAX_INDEX_NAME_LEN {
224            return Err("corrupted IndexName: invalid length");
225        }
226        if !bytes[2..2 + len].is_ascii() {
227            return Err("corrupted IndexName: invalid encoding");
228        }
229        if bytes[2 + len..].iter().any(|&b| b != 0) {
230            return Err("corrupted IndexName: non-zero padding");
231        }
232
233        let mut name = [0u8; MAX_INDEX_NAME_LEN];
234        name.copy_from_slice(&bytes[2..]);
235
236        Ok(Self {
237            len: len as u16,
238            bytes: name,
239        })
240    }
241
242    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
243        Self::from_bytes(bytes)
244    }
245
246    fn push_ascii(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
247        assert!(bytes.is_ascii(), "index name must be ASCII");
248        assert!(
249            *len + bytes.len() <= MAX_INDEX_NAME_LEN,
250            "index name too long"
251        );
252
253        out[*len..*len + bytes.len()].copy_from_slice(bytes);
254        *len += bytes.len();
255    }
256
257    #[must_use]
258    pub const fn max_storable() -> Self {
259        Self {
260            len: MAX_INDEX_NAME_LEN as u16,
261            bytes: [b'z'; MAX_INDEX_NAME_LEN],
262        }
263    }
264}
265
266impl TryFrom<&[u8]> for IndexName {
267    type Error = &'static str;
268
269    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
270        Self::try_from_bytes(bytes)
271    }
272}
273
274impl fmt::Debug for IndexName {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        write!(f, "IndexName({})", self.as_str())
277    }
278}
279
280impl Display for IndexName {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        write!(f, "{}", self.as_str())
283    }
284}
285
286impl Ord for IndexName {
287    fn cmp(&self, other: &Self) -> Ordering {
288        self.len
289            .cmp(&other.len)
290            .then_with(|| self.bytes.cmp(&other.bytes))
291    }
292}
293
294impl PartialOrd for IndexName {
295    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
296        Some(self.cmp(other))
297    }
298}
299
300///
301/// TESTS
302///
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    const ENTITY_64: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
309    const ENTITY_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
310    const FIELD_64_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
311    const FIELD_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
312    const FIELD_64_C: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
313    const FIELD_64_D: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
314
315    #[test]
316    fn index_name_max_len_matches_limits() {
317        let entity = EntityName::from_static(ENTITY_64);
318        let fields = [FIELD_64_A, FIELD_64_B, FIELD_64_C, FIELD_64_D];
319
320        assert_eq!(entity.as_str().len(), MAX_ENTITY_NAME_LEN);
321        for field in &fields {
322            assert_eq!(field.len(), MAX_INDEX_FIELD_NAME_LEN);
323        }
324        assert_eq!(fields.len(), MAX_INDEX_FIELDS);
325
326        let name = IndexName::from_parts(&entity, &fields);
327
328        assert_eq!(name.as_bytes().len(), MAX_INDEX_NAME_LEN);
329    }
330
331    #[test]
332    fn index_name_max_size_roundtrip_and_ordering() {
333        let entity_a = EntityName::from_static(ENTITY_64);
334        let entity_b = EntityName::from_static(ENTITY_64_B);
335        let fields_a = [FIELD_64_A, FIELD_64_A, FIELD_64_A, FIELD_64_A];
336        let fields_b = [FIELD_64_B, FIELD_64_B, FIELD_64_B, FIELD_64_B];
337
338        let idx_a = IndexName::from_parts(&entity_a, &fields_a);
339        let idx_b = IndexName::from_parts(&entity_b, &fields_b);
340
341        assert_eq!(idx_a.as_bytes().len(), MAX_INDEX_NAME_LEN);
342        assert_eq!(idx_b.as_bytes().len(), MAX_INDEX_NAME_LEN);
343
344        let decoded = IndexName::from_bytes(&idx_a.to_bytes()).unwrap();
345        assert_eq!(idx_a, decoded);
346
347        assert_eq!(idx_a.cmp(&idx_b), idx_a.to_bytes().cmp(&idx_b.to_bytes()));
348    }
349
350    #[test]
351    #[should_panic(expected = "index has too many fields")]
352    fn rejects_too_many_index_fields() {
353        let entity = EntityName::from_static("entity");
354        let fields = ["a", "b", "c", "d", "e"];
355        let _ = IndexName::from_parts(&entity, &fields);
356    }
357
358    #[test]
359    #[should_panic(expected = "index field name too long")]
360    fn rejects_index_field_over_len() {
361        let entity = EntityName::from_static("entity");
362        let long_field = "a".repeat(MAX_INDEX_FIELD_NAME_LEN + 1);
363        let fields = [long_field.as_str()];
364        let _ = IndexName::from_parts(&entity, &fields);
365    }
366
367    #[test]
368    fn entity_from_static_roundtrip() {
369        let e = EntityName::from_static("user");
370        assert_eq!(e.len(), 4);
371        assert_eq!(e.as_str(), "user");
372    }
373
374    #[test]
375    #[should_panic(expected = "entity name length out of bounds")]
376    fn entity_rejects_empty() {
377        let _ = EntityName::from_static("");
378    }
379
380    #[test]
381    #[should_panic(expected = "entity name must be ASCII")]
382    fn entity_rejects_non_ascii() {
383        let _ = EntityName::from_static("usér");
384    }
385
386    #[test]
387    fn entity_storage_roundtrip() {
388        let e = EntityName::from_static("entity_name");
389        let bytes = e.to_bytes();
390        let decoded = EntityName::from_bytes(&bytes).unwrap();
391        assert_eq!(e, decoded);
392    }
393
394    #[test]
395    fn entity_rejects_invalid_size() {
396        let buf = vec![0u8; EntityName::STORED_SIZE_USIZE - 1];
397        assert!(EntityName::from_bytes(&buf).is_err());
398    }
399
400    #[test]
401    fn entity_rejects_invalid_size_oversized() {
402        let buf = vec![0u8; EntityName::STORED_SIZE_USIZE + 1];
403        assert!(EntityName::from_bytes(&buf).is_err());
404    }
405
406    #[test]
407    fn entity_rejects_len_over_max() {
408        let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
409        buf[0] = (MAX_ENTITY_NAME_LEN as u8).saturating_add(1);
410        assert!(EntityName::from_bytes(&buf).is_err());
411    }
412
413    #[test]
414    fn entity_rejects_non_ascii_from_bytes() {
415        let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
416        buf[0] = 1;
417        buf[1] = 0xFF;
418        assert!(EntityName::from_bytes(&buf).is_err());
419    }
420
421    #[test]
422    fn entity_rejects_non_zero_padding() {
423        let e = EntityName::from_static("user");
424        let mut bytes = e.to_bytes();
425        bytes[1 + e.len()] = b'x';
426        assert!(EntityName::from_bytes(&bytes).is_err());
427    }
428
429    #[test]
430    fn entity_ordering_matches_bytes() {
431        let a = EntityName::from_static("abc");
432        let b = EntityName::from_static("abd");
433        let c = EntityName::from_static("abcx");
434
435        assert_eq!(a.cmp(&b), a.to_bytes().cmp(&b.to_bytes()));
436        assert_eq!(a.cmp(&c), a.to_bytes().cmp(&c.to_bytes()));
437    }
438
439    #[test]
440    fn entity_ordering_b_vs_aa() {
441        let b = EntityName::from_static("b");
442        let aa = EntityName::from_static("aa");
443        assert_eq!(b.cmp(&aa), b.to_bytes().cmp(&aa.to_bytes()));
444    }
445
446    #[test]
447    fn entity_ordering_prefix_matches_bytes() {
448        let a = EntityName::from_static("a");
449        let aa = EntityName::from_static("aa");
450        assert_eq!(a.cmp(&aa), a.to_bytes().cmp(&aa.to_bytes()));
451    }
452
453    #[test]
454    fn index_single_field_format() {
455        let entity = EntityName::from_static("user");
456        let idx = IndexName::from_parts(&entity, &["email"]);
457
458        assert_eq!(idx.as_str(), "user|email");
459    }
460
461    #[test]
462    fn index_field_order_is_preserved() {
463        let entity = EntityName::from_static("user");
464        let idx = IndexName::from_parts(&entity, &["a", "b", "c"]);
465
466        assert_eq!(idx.as_str(), "user|a|b|c");
467    }
468
469    #[test]
470    fn index_storage_roundtrip() {
471        let entity = EntityName::from_static("user");
472        let idx = IndexName::from_parts(&entity, &["a", "b"]);
473
474        let bytes = idx.to_bytes();
475        let decoded = IndexName::from_bytes(&bytes).unwrap();
476
477        assert_eq!(idx, decoded);
478    }
479
480    #[test]
481    fn index_rejects_zero_len() {
482        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
483        buf[0] = 0;
484        assert!(IndexName::from_bytes(&buf).is_err());
485    }
486
487    #[test]
488    fn index_rejects_invalid_size_oversized() {
489        let buf = vec![0u8; IndexName::STORED_SIZE_USIZE + 1];
490        assert!(IndexName::from_bytes(&buf).is_err());
491    }
492
493    #[test]
494    fn index_rejects_len_over_max() {
495        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
496        let len = (MAX_INDEX_NAME_LEN as u16).saturating_add(1);
497        buf[..2].copy_from_slice(&len.to_be_bytes());
498        assert!(IndexName::from_bytes(&buf).is_err());
499    }
500
501    #[test]
502    fn index_rejects_non_ascii_from_bytes() {
503        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
504        buf[..2].copy_from_slice(&1u16.to_be_bytes());
505        buf[2] = 0xFF;
506        assert!(IndexName::from_bytes(&buf).is_err());
507    }
508
509    #[test]
510    fn index_rejects_non_zero_padding() {
511        let entity = EntityName::from_static("user");
512        let idx = IndexName::from_parts(&entity, &["a"]);
513        let mut bytes = idx.to_bytes();
514        bytes[2 + idx.len as usize] = b'x';
515        assert!(IndexName::from_bytes(&bytes).is_err());
516    }
517
518    #[test]
519    fn index_ordering_matches_bytes() {
520        let entity = EntityName::from_static("user");
521
522        let a = IndexName::from_parts(&entity, &["a"]);
523        let ab = IndexName::from_parts(&entity, &["a", "b"]);
524        let b = IndexName::from_parts(&entity, &["b"]);
525
526        assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
527        assert_eq!(ab.cmp(&b), ab.to_bytes().cmp(&b.to_bytes()));
528    }
529
530    #[test]
531    fn index_ordering_prefix_matches_bytes() {
532        let entity = EntityName::from_static("user");
533        let a = IndexName::from_parts(&entity, &["a"]);
534        let ab = IndexName::from_parts(&entity, &["a", "b"]);
535        assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
536    }
537
538    #[test]
539    fn max_storable_orders_last() {
540        let entity = EntityName::from_static("zz");
541        let max = EntityName::max_storable();
542
543        assert!(entity < max);
544    }
545
546    ///
547    /// FUZZING
548    ///
549
550    /// Simple deterministic ASCII generator
551    fn gen_ascii(seed: u64, max_len: usize) -> String {
552        let len = (seed as usize % max_len).max(1);
553        let mut out = String::with_capacity(len);
554
555        let mut x = seed;
556        for _ in 0..len {
557            // printable ASCII range [a–z]
558            x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
559            let c = b'a' + (x % 26) as u8;
560            out.push(c as char);
561        }
562
563        out
564    }
565
566    #[test]
567    fn fuzz_entity_name_roundtrip_and_ordering() {
568        const RUNS: u64 = 1_000;
569
570        let mut prev: Option<EntityName> = None;
571
572        for i in 1..=RUNS {
573            let s = gen_ascii(i, MAX_ENTITY_NAME_LEN);
574            let e = EntityName::from_static(Box::leak(s.clone().into_boxed_str()));
575
576            // Round-trip
577            let bytes = e.to_bytes();
578            let decoded = EntityName::from_bytes(&bytes).unwrap();
579            assert_eq!(e, decoded);
580
581            // Ordering vs bytes
582            if let Some(p) = prev {
583                let ord_entity = p.cmp(&e);
584                let ord_bytes = p.to_bytes().cmp(&e.to_bytes());
585                assert_eq!(ord_entity, ord_bytes);
586            }
587
588            prev = Some(e);
589        }
590    }
591
592    #[test]
593    fn fuzz_index_name_roundtrip_and_ordering() {
594        const RUNS: u64 = 1_000;
595
596        let entity = EntityName::from_static("entity");
597        let mut prev: Option<IndexName> = None;
598
599        for i in 1..=RUNS {
600            let field_count = (i as usize % MAX_INDEX_FIELDS).max(1);
601
602            let mut field_strings = Vec::with_capacity(field_count);
603            let mut fields = Vec::with_capacity(field_count);
604            let mut string_parts = Vec::with_capacity(field_count + 1);
605
606            string_parts.push(entity.as_str().to_owned());
607
608            for f in 0..field_count {
609                let s = gen_ascii(i * 31 + f as u64, MAX_INDEX_FIELD_NAME_LEN);
610                string_parts.push(s.clone());
611                field_strings.push(s);
612            }
613
614            for s in &field_strings {
615                fields.push(s.as_str());
616            }
617
618            let idx = IndexName::from_parts(&entity, &fields);
619            let expected = string_parts.join("|");
620
621            // Structural correctness
622            assert_eq!(idx.as_str(), expected);
623
624            // Round-trip
625            let bytes = idx.to_bytes();
626            let decoded = IndexName::from_bytes(&bytes).unwrap();
627            assert_eq!(idx, decoded);
628
629            // Ordering vs bytes
630            if let Some(p_idx) = prev {
631                let ord_idx = p_idx.cmp(&idx);
632                let ord_bytes = p_idx.to_bytes().cmp(&idx.to_bytes());
633                assert_eq!(ord_idx, ord_bytes);
634            }
635
636            prev = Some(idx);
637        }
638    }
639}