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 = 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    #[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: u16,
153    pub bytes: [u8; MAX_INDEX_NAME_LEN],
154}
155
156impl IndexName {
157    pub const STORED_SIZE: u32 = 2 + 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 u16,
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[..2].copy_from_slice(&self.len.to_be_bytes());
201        out[2..].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("corrupted IndexName: invalid size");
208        }
209
210        let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
211        if len == 0 || len > MAX_INDEX_NAME_LEN {
212            return Err("corrupted IndexName: invalid length");
213        }
214        if !bytes[2..2 + len].is_ascii() {
215            return Err("corrupted IndexName: invalid encoding");
216        }
217        if bytes[2 + len..].iter().any(|&b| b != 0) {
218            return Err("corrupted IndexName: non-zero padding");
219        }
220
221        let mut name = [0u8; MAX_INDEX_NAME_LEN];
222        name.copy_from_slice(&bytes[2..]);
223
224        Ok(Self {
225            len: len as u16,
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 u16,
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_64: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
285    const ENTITY_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
286    const FIELD_64_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
287    const FIELD_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
288    const FIELD_64_C: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
289    const FIELD_64_D: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
290
291    #[test]
292    fn index_name_max_len_matches_limits() {
293        let entity = EntityName::from_static(ENTITY_64);
294        let fields = [FIELD_64_A, FIELD_64_B, FIELD_64_C, FIELD_64_D];
295
296        assert_eq!(entity.as_str().len(), MAX_ENTITY_NAME_LEN);
297        for field in &fields {
298            assert_eq!(field.len(), MAX_INDEX_FIELD_NAME_LEN);
299        }
300        assert_eq!(fields.len(), MAX_INDEX_FIELDS);
301
302        let name = IndexName::from_parts(&entity, &fields);
303
304        assert_eq!(name.as_bytes().len(), MAX_INDEX_NAME_LEN);
305    }
306
307    #[test]
308    fn index_name_max_size_roundtrip_and_ordering() {
309        let entity_a = EntityName::from_static(ENTITY_64);
310        let entity_b = EntityName::from_static(ENTITY_64_B);
311        let fields_a = [FIELD_64_A, FIELD_64_A, FIELD_64_A, FIELD_64_A];
312        let fields_b = [FIELD_64_B, FIELD_64_B, FIELD_64_B, FIELD_64_B];
313
314        let idx_a = IndexName::from_parts(&entity_a, &fields_a);
315        let idx_b = IndexName::from_parts(&entity_b, &fields_b);
316
317        assert_eq!(idx_a.as_bytes().len(), MAX_INDEX_NAME_LEN);
318        assert_eq!(idx_b.as_bytes().len(), MAX_INDEX_NAME_LEN);
319
320        let decoded = IndexName::from_bytes(&idx_a.to_bytes()).unwrap();
321        assert_eq!(idx_a, decoded);
322
323        assert_eq!(idx_a.cmp(&idx_b), idx_a.to_bytes().cmp(&idx_b.to_bytes()));
324    }
325
326    #[test]
327    #[should_panic(expected = "index has too many fields")]
328    fn rejects_too_many_index_fields() {
329        let entity = EntityName::from_static("entity");
330        let fields = ["a", "b", "c", "d", "e"];
331        let _ = IndexName::from_parts(&entity, &fields);
332    }
333
334    #[test]
335    #[should_panic(expected = "index field name too long")]
336    fn rejects_index_field_over_len() {
337        let entity = EntityName::from_static("entity");
338        let long_field = "a".repeat(MAX_INDEX_FIELD_NAME_LEN + 1);
339        let fields = [long_field.as_str()];
340        let _ = IndexName::from_parts(&entity, &fields);
341    }
342
343    #[test]
344    fn entity_from_static_roundtrip() {
345        let e = EntityName::from_static("user");
346        assert_eq!(e.len(), 4);
347        assert_eq!(e.as_str(), "user");
348    }
349
350    #[test]
351    #[should_panic(expected = "entity name length out of bounds")]
352    fn entity_rejects_empty() {
353        let _ = EntityName::from_static("");
354    }
355
356    #[test]
357    #[should_panic(expected = "entity name must be ASCII")]
358    fn entity_rejects_non_ascii() {
359        let _ = EntityName::from_static("usér");
360    }
361
362    #[test]
363    fn entity_storage_roundtrip() {
364        let e = EntityName::from_static("entity_name");
365        let bytes = e.to_bytes();
366        let decoded = EntityName::from_bytes(&bytes).unwrap();
367        assert_eq!(e, decoded);
368    }
369
370    #[test]
371    fn entity_rejects_invalid_size() {
372        let buf = vec![0u8; EntityName::STORED_SIZE_USIZE - 1];
373        assert!(EntityName::from_bytes(&buf).is_err());
374    }
375
376    #[test]
377    fn entity_rejects_invalid_size_oversized() {
378        let buf = vec![0u8; EntityName::STORED_SIZE_USIZE + 1];
379        assert!(EntityName::from_bytes(&buf).is_err());
380    }
381
382    #[test]
383    fn entity_rejects_len_over_max() {
384        let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
385        buf[0] = (MAX_ENTITY_NAME_LEN as u8).saturating_add(1);
386        assert!(EntityName::from_bytes(&buf).is_err());
387    }
388
389    #[test]
390    fn entity_rejects_non_ascii_from_bytes() {
391        let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
392        buf[0] = 1;
393        buf[1] = 0xFF;
394        assert!(EntityName::from_bytes(&buf).is_err());
395    }
396
397    #[test]
398    fn entity_rejects_non_zero_padding() {
399        let e = EntityName::from_static("user");
400        let mut bytes = e.to_bytes();
401        bytes[1 + e.len()] = b'x';
402        assert!(EntityName::from_bytes(&bytes).is_err());
403    }
404
405    #[test]
406    fn entity_ordering_matches_bytes() {
407        let a = EntityName::from_static("abc");
408        let b = EntityName::from_static("abd");
409        let c = EntityName::from_static("abcx");
410
411        assert_eq!(a.cmp(&b), a.to_bytes().cmp(&b.to_bytes()));
412        assert_eq!(a.cmp(&c), a.to_bytes().cmp(&c.to_bytes()));
413    }
414
415    #[test]
416    fn entity_ordering_b_vs_aa() {
417        let b = EntityName::from_static("b");
418        let aa = EntityName::from_static("aa");
419        assert_eq!(b.cmp(&aa), b.to_bytes().cmp(&aa.to_bytes()));
420    }
421
422    #[test]
423    fn entity_ordering_prefix_matches_bytes() {
424        let a = EntityName::from_static("a");
425        let aa = EntityName::from_static("aa");
426        assert_eq!(a.cmp(&aa), a.to_bytes().cmp(&aa.to_bytes()));
427    }
428
429    #[test]
430    fn index_single_field_format() {
431        let entity = EntityName::from_static("user");
432        let idx = IndexName::from_parts(&entity, &["email"]);
433
434        assert_eq!(idx.as_str(), "user|email");
435    }
436
437    #[test]
438    fn index_field_order_is_preserved() {
439        let entity = EntityName::from_static("user");
440        let idx = IndexName::from_parts(&entity, &["a", "b", "c"]);
441
442        assert_eq!(idx.as_str(), "user|a|b|c");
443    }
444
445    #[test]
446    fn index_storage_roundtrip() {
447        let entity = EntityName::from_static("user");
448        let idx = IndexName::from_parts(&entity, &["a", "b"]);
449
450        let bytes = idx.to_bytes();
451        let decoded = IndexName::from_bytes(&bytes).unwrap();
452
453        assert_eq!(idx, decoded);
454    }
455
456    #[test]
457    fn index_rejects_zero_len() {
458        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
459        buf[0] = 0;
460        assert!(IndexName::from_bytes(&buf).is_err());
461    }
462
463    #[test]
464    fn index_rejects_invalid_size_oversized() {
465        let buf = vec![0u8; IndexName::STORED_SIZE_USIZE + 1];
466        assert!(IndexName::from_bytes(&buf).is_err());
467    }
468
469    #[test]
470    fn index_rejects_len_over_max() {
471        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
472        let len = (MAX_INDEX_NAME_LEN as u16).saturating_add(1);
473        buf[..2].copy_from_slice(&len.to_be_bytes());
474        assert!(IndexName::from_bytes(&buf).is_err());
475    }
476
477    #[test]
478    fn index_rejects_non_ascii_from_bytes() {
479        let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
480        buf[..2].copy_from_slice(&1u16.to_be_bytes());
481        buf[2] = 0xFF;
482        assert!(IndexName::from_bytes(&buf).is_err());
483    }
484
485    #[test]
486    fn index_rejects_non_zero_padding() {
487        let entity = EntityName::from_static("user");
488        let idx = IndexName::from_parts(&entity, &["a"]);
489        let mut bytes = idx.to_bytes();
490        bytes[2 + idx.len as usize] = b'x';
491        assert!(IndexName::from_bytes(&bytes).is_err());
492    }
493
494    #[test]
495    fn index_ordering_matches_bytes() {
496        let entity = EntityName::from_static("user");
497
498        let a = IndexName::from_parts(&entity, &["a"]);
499        let ab = IndexName::from_parts(&entity, &["a", "b"]);
500        let b = IndexName::from_parts(&entity, &["b"]);
501
502        assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
503        assert_eq!(ab.cmp(&b), ab.to_bytes().cmp(&b.to_bytes()));
504    }
505
506    #[test]
507    fn index_ordering_prefix_matches_bytes() {
508        let entity = EntityName::from_static("user");
509        let a = IndexName::from_parts(&entity, &["a"]);
510        let ab = IndexName::from_parts(&entity, &["a", "b"]);
511        assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
512    }
513
514    #[test]
515    fn max_storable_orders_last() {
516        let entity = EntityName::from_static("zz");
517        let max = EntityName::max_storable();
518
519        assert!(entity < max);
520    }
521
522    ///
523    /// FUZZING
524    ///
525
526    /// Simple deterministic ASCII generator
527    fn gen_ascii(seed: u64, max_len: usize) -> String {
528        let len = (seed as usize % max_len).max(1);
529        let mut out = String::with_capacity(len);
530
531        let mut x = seed;
532        for _ in 0..len {
533            // printable ASCII range [a–z]
534            x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
535            let c = b'a' + (x % 26) as u8;
536            out.push(c as char);
537        }
538
539        out
540    }
541
542    #[test]
543    fn fuzz_entity_name_roundtrip_and_ordering() {
544        const RUNS: u64 = 1_000;
545
546        let mut prev: Option<EntityName> = None;
547
548        for i in 1..=RUNS {
549            let s = gen_ascii(i, MAX_ENTITY_NAME_LEN);
550            let e = EntityName::from_static(Box::leak(s.clone().into_boxed_str()));
551
552            // Round-trip
553            let bytes = e.to_bytes();
554            let decoded = EntityName::from_bytes(&bytes).unwrap();
555            assert_eq!(e, decoded);
556
557            // Ordering vs bytes
558            if let Some(p) = prev {
559                let ord_entity = p.cmp(&e);
560                let ord_bytes = p.to_bytes().cmp(&e.to_bytes());
561                assert_eq!(ord_entity, ord_bytes);
562            }
563
564            prev = Some(e);
565        }
566    }
567
568    #[test]
569    fn fuzz_index_name_roundtrip_and_ordering() {
570        const RUNS: u64 = 1_000;
571
572        let entity = EntityName::from_static("entity");
573        let mut prev: Option<IndexName> = None;
574
575        for i in 1..=RUNS {
576            let field_count = (i as usize % MAX_INDEX_FIELDS).max(1);
577
578            let mut field_strings = Vec::with_capacity(field_count);
579            let mut fields = Vec::with_capacity(field_count);
580            let mut string_parts = Vec::with_capacity(field_count + 1);
581
582            string_parts.push(entity.as_str().to_owned());
583
584            for f in 0..field_count {
585                let s = gen_ascii(i * 31 + f as u64, MAX_INDEX_FIELD_NAME_LEN);
586                string_parts.push(s.clone());
587                field_strings.push(s);
588            }
589
590            for s in &field_strings {
591                fields.push(s.as_str());
592            }
593
594            let idx = IndexName::from_parts(&entity, &fields);
595            let expected = string_parts.join("|");
596
597            // Structural correctness
598            assert_eq!(idx.as_str(), expected);
599
600            // Round-trip
601            let bytes = idx.to_bytes();
602            let decoded = IndexName::from_bytes(&bytes).unwrap();
603            assert_eq!(idx, decoded);
604
605            // Ordering vs bytes
606            if let Some(p_idx) = prev {
607                let ord_idx = p_idx.cmp(&idx);
608                let ord_bytes = p_idx.to_bytes().cmp(&idx.to_bytes());
609                assert_eq!(ord_idx, ord_bytes);
610            }
611
612            prev = Some(idx);
613        }
614    }
615}