Skip to main content

icydb_core/db/identity/
mod.rs

1//! Module: identity
2//! Responsibility: validated entity/index naming and stable byte ordering contracts.
3//! Does not own: schema metadata, relation policy, or storage-layer persistence.
4//! Boundary: all identity construction/decoding for db data/index key domains.
5//!
6//! Invariants:
7//! - Identities are ASCII, non-empty, and bounded by MAX_* limits.
8//! - All construction paths validate invariants.
9//! - Stored byte representation is canonical and order-preserving.
10//! - Ordering semantics follow the length-prefixed stored-byte layout, not
11//!   lexicographic string ordering.
12
13#![expect(clippy::cast_possible_truncation)]
14
15#[cfg(test)]
16mod tests;
17
18use crate::MAX_INDEX_FIELDS;
19use std::{
20    cmp::Ordering,
21    fmt::{self, Display},
22};
23use thiserror::Error as ThisError;
24
25///
26/// Constants
27///
28
29pub(super) const MAX_ENTITY_NAME_LEN: usize = 64;
30pub(super) const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
31pub(super) const MAX_INDEX_NAME_LEN: usize =
32    MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
33const INDEX_NAME_SEGMENT_DELIMITER: u8 = b'|';
34const MAX_ASCII_BYTE: u8 = 0x7F;
35
36///
37/// IdentityDecodeError
38/// Decode errors (storage / corruption boundary)
39///
40
41#[derive(Debug, ThisError)]
42pub enum IdentityDecodeError {
43    #[error("invalid size")]
44    InvalidSize,
45
46    #[error("invalid length")]
47    InvalidLength,
48
49    #[error("non-ascii encoding")]
50    NonAscii,
51
52    #[error("non-zero padding")]
53    NonZeroPadding,
54
55    #[error("reserved identity delimiter")]
56    Delimiter,
57}
58
59///
60/// EntityNameError
61///
62
63#[derive(Debug, ThisError)]
64pub enum EntityNameError {
65    #[error("entity name is empty")]
66    Empty,
67
68    #[error("entity name length {len} exceeds max {max}")]
69    TooLong { len: usize, max: usize },
70
71    #[error("entity name must be ASCII")]
72    NonAscii,
73
74    #[error("entity name must not contain '|'")]
75    Delimiter,
76}
77
78///
79/// IndexNameError
80///
81
82#[derive(Debug, ThisError)]
83pub enum IndexNameError {
84    #[error("index has {len} fields (max {max})")]
85    TooManyFields { len: usize, max: usize },
86
87    #[error("index must reference at least one field")]
88    NoFields,
89
90    #[error("index field name is empty")]
91    FieldEmpty,
92
93    #[error("index field name '{field}' exceeds max length {max}")]
94    FieldTooLong { field: String, max: usize },
95
96    #[error("index field name '{field}' must be ASCII")]
97    FieldNonAscii { field: String },
98
99    #[error("index field name '{field}' must not contain '|'")]
100    FieldDelimiter { field: String },
101
102    #[error("index name length {len} exceeds max {max}")]
103    TooLong { len: usize, max: usize },
104}
105
106///
107/// EntityName
108///
109
110#[derive(Clone, Copy, Eq, Hash, PartialEq)]
111pub struct EntityName {
112    len: u8,
113    bytes: [u8; MAX_ENTITY_NAME_LEN],
114}
115
116impl EntityName {
117    /// Fixed on-disk size in bytes (stable, protocol-level)
118    pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
119
120    /// Fixed in-memory size (for buffers and arrays)
121    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
122
123    /// Validate and construct an entity name from one ASCII string.
124    pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
125        // Phase 1: validate user-visible identity constraints.
126        let bytes = name.as_bytes();
127        let len = bytes.len();
128
129        if len == 0 {
130            return Err(EntityNameError::Empty);
131        }
132        if len > MAX_ENTITY_NAME_LEN {
133            return Err(EntityNameError::TooLong {
134                len,
135                max: MAX_ENTITY_NAME_LEN,
136            });
137        }
138        if !bytes.is_ascii() {
139            return Err(EntityNameError::NonAscii);
140        }
141        if bytes.contains(&INDEX_NAME_SEGMENT_DELIMITER) {
142            return Err(EntityNameError::Delimiter);
143        }
144
145        // Phase 2: write into fixed-size canonical storage.
146        let mut out = [0u8; MAX_ENTITY_NAME_LEN];
147        out[..len].copy_from_slice(bytes);
148
149        Ok(Self {
150            len: len as u8,
151            bytes: out,
152        })
153    }
154
155    /// Return the stored entity-name length.
156    #[must_use]
157    pub const fn len(&self) -> usize {
158        self.len as usize
159    }
160
161    /// Return whether the stored entity-name length is zero.
162    #[must_use]
163    pub const fn is_empty(&self) -> bool {
164        self.len() == 0
165    }
166
167    /// Borrow raw identity bytes excluding trailing fixed-buffer padding.
168    #[must_use]
169    pub fn as_bytes(&self) -> &[u8] {
170        &self.bytes[..self.len()]
171    }
172
173    /// Borrow the entity name as UTF-8 text.
174    #[must_use]
175    pub fn as_str(&self) -> &str {
176        // Invariant: construction and decoding enforce ASCII-only storage,
177        // so UTF-8 decoding cannot fail.
178        std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
179    }
180
181    /// Encode this identity into its fixed-size persisted representation.
182    #[must_use]
183    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
184        let mut out = [0u8; Self::STORED_SIZE_USIZE];
185        out[0] = self.len;
186        out[1..].copy_from_slice(&self.bytes);
187        out
188    }
189
190    /// Decode one fixed-size persisted entity identity payload.
191    pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
192        // Phase 1: validate layout and payload bounds.
193        if bytes.len() != Self::STORED_SIZE_USIZE {
194            return Err(IdentityDecodeError::InvalidSize);
195        }
196
197        let len = bytes[0] as usize;
198        if len == 0 || len > MAX_ENTITY_NAME_LEN {
199            return Err(IdentityDecodeError::InvalidLength);
200        }
201        if !bytes[1..=len].is_ascii() {
202            return Err(IdentityDecodeError::NonAscii);
203        }
204        if bytes[1..=len].contains(&INDEX_NAME_SEGMENT_DELIMITER) {
205            return Err(IdentityDecodeError::Delimiter);
206        }
207        if bytes[1 + len..].iter().any(|&b| b != 0) {
208            return Err(IdentityDecodeError::NonZeroPadding);
209        }
210
211        // Phase 2: materialize canonical fixed-buffer identity storage.
212        let mut name = [0u8; MAX_ENTITY_NAME_LEN];
213        name.copy_from_slice(&bytes[1..]);
214
215        Ok(Self {
216            len: len as u8,
217            bytes: name,
218        })
219    }
220
221    /// Return a maximal sortable entity identity sentinel value.
222    #[must_use]
223    pub const fn max_storable() -> Self {
224        Self {
225            len: MAX_ENTITY_NAME_LEN as u8,
226            bytes: [MAX_ASCII_BYTE; MAX_ENTITY_NAME_LEN],
227        }
228    }
229}
230
231impl Ord for EntityName {
232    fn cmp(&self, other: &Self) -> Ordering {
233        // Keep ordering consistent with `to_bytes()` (length prefix first).
234        // This is deterministic protocol/storage ordering, not lexical string order.
235        self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
236    }
237}
238
239impl PartialOrd for EntityName {
240    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
241        Some(self.cmp(other))
242    }
243}
244
245impl Display for EntityName {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        f.write_str(self.as_str())
248    }
249}
250
251impl fmt::Debug for EntityName {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        write!(f, "EntityName({})", self.as_str())
254    }
255}
256
257///
258/// IndexName
259///
260
261#[derive(Clone, Copy, Eq, Hash, PartialEq)]
262pub struct IndexName {
263    len: u16,
264    bytes: [u8; MAX_INDEX_NAME_LEN],
265}
266
267impl IndexName {
268    /// Fixed on-disk size in bytes (stable, protocol-level).
269    pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
270    /// Fixed in-memory size (for buffers and arrays).
271    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
272
273    /// Validate and construct one index identity from an entity + field list.
274    pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
275        // Phase 1: validate index-field count and per-field identity constraints.
276        if fields.is_empty() {
277            return Err(IndexNameError::NoFields);
278        }
279        if fields.len() > MAX_INDEX_FIELDS {
280            return Err(IndexNameError::TooManyFields {
281                len: fields.len(),
282                max: MAX_INDEX_FIELDS,
283            });
284        }
285
286        let mut total_len = entity.len();
287        for field in fields {
288            let field_len = field.len();
289            if field_len == 0 {
290                return Err(IndexNameError::FieldEmpty);
291            }
292            if field_len > MAX_INDEX_FIELD_NAME_LEN {
293                return Err(IndexNameError::FieldTooLong {
294                    field: (*field).to_string(),
295                    max: MAX_INDEX_FIELD_NAME_LEN,
296                });
297            }
298            if !field.is_ascii() {
299                return Err(IndexNameError::FieldNonAscii {
300                    field: (*field).to_string(),
301                });
302            }
303            if field.as_bytes().contains(&INDEX_NAME_SEGMENT_DELIMITER) {
304                return Err(IndexNameError::FieldDelimiter {
305                    field: (*field).to_string(),
306                });
307            }
308            total_len = total_len.saturating_add(1 + field_len);
309        }
310
311        if total_len > MAX_INDEX_NAME_LEN {
312            return Err(IndexNameError::TooLong {
313                len: total_len,
314                max: MAX_INDEX_NAME_LEN,
315            });
316        }
317
318        // Phase 2: encode canonical `entity|field...` bytes into fixed storage.
319        let mut out = [0u8; MAX_INDEX_NAME_LEN];
320        let mut len = 0usize;
321
322        Self::push_bytes(&mut out, &mut len, entity.as_bytes());
323        for field in fields {
324            Self::push_bytes(&mut out, &mut len, b"|");
325            Self::push_bytes(&mut out, &mut len, field.as_bytes());
326        }
327
328        Ok(Self {
329            len: len as u16,
330            bytes: out,
331        })
332    }
333
334    /// Borrow raw index-identity bytes excluding trailing fixed-buffer padding.
335    #[must_use]
336    pub fn as_bytes(&self) -> &[u8] {
337        &self.bytes[..self.len as usize]
338    }
339
340    /// Borrow the index identity as UTF-8 text.
341    #[must_use]
342    pub fn as_str(&self) -> &str {
343        // Invariant: construction and decoding enforce ASCII-only storage,
344        // so UTF-8 decoding cannot fail.
345        std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
346    }
347
348    /// Encode this identity into its fixed-size persisted representation.
349    #[must_use]
350    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
351        let mut out = [0u8; Self::STORED_SIZE_USIZE];
352        out[..2].copy_from_slice(&self.len.to_be_bytes());
353        out[2..].copy_from_slice(&self.bytes);
354        out
355    }
356
357    /// Decode one fixed-size persisted index identity payload.
358    ///
359    /// This validates the canonical fixed-width byte envelope only. It does not
360    /// reconstruct field segments or prove the bytes were produced by
361    /// `try_from_parts`; callers must ensure persisted bytes originate from a
362    /// previously validated `IndexName`.
363    pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
364        // Phase 1: validate layout and payload bounds.
365        if bytes.len() != Self::STORED_SIZE_USIZE {
366            return Err(IdentityDecodeError::InvalidSize);
367        }
368
369        let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
370        if len == 0 || len > MAX_INDEX_NAME_LEN {
371            return Err(IdentityDecodeError::InvalidLength);
372        }
373        if !bytes[2..2 + len].is_ascii() {
374            return Err(IdentityDecodeError::NonAscii);
375        }
376        if bytes[2 + len..].iter().any(|&b| b != 0) {
377            return Err(IdentityDecodeError::NonZeroPadding);
378        }
379
380        // Phase 2: materialize canonical fixed-buffer identity storage.
381        let mut name = [0u8; MAX_INDEX_NAME_LEN];
382        name.copy_from_slice(&bytes[2..]);
383
384        Ok(Self {
385            len: len as u16,
386            bytes: name,
387        })
388    }
389
390    // Append bytes into the fixed-size identity buffer while tracking write offset.
391    fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
392        let end = *len + bytes.len();
393        out[*len..end].copy_from_slice(bytes);
394        *len = end;
395    }
396
397    /// Return a maximal sortable index identity sentinel value.
398    #[must_use]
399    pub const fn max_storable() -> Self {
400        Self {
401            len: MAX_INDEX_NAME_LEN as u16,
402            bytes: [MAX_ASCII_BYTE; MAX_INDEX_NAME_LEN],
403        }
404    }
405}
406
407impl Ord for IndexName {
408    fn cmp(&self, other: &Self) -> Ordering {
409        self.to_bytes().cmp(&other.to_bytes())
410    }
411}
412
413impl PartialOrd for IndexName {
414    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
415        Some(self.cmp(other))
416    }
417}
418
419impl fmt::Debug for IndexName {
420    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421        write!(f, "IndexName({})", self.as_str())
422    }
423}
424
425impl Display for IndexName {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        f.write_str(self.as_str())
428    }
429}