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));
33
34///
35/// IdentityDecodeError
36/// Decode errors (storage / corruption boundary)
37///
38
39#[derive(Debug, ThisError)]
40pub enum IdentityDecodeError {
41    #[error("invalid size")]
42    InvalidSize,
43
44    #[error("invalid length")]
45    InvalidLength,
46
47    #[error("non-ascii encoding")]
48    NonAscii,
49
50    #[error("non-zero padding")]
51    NonZeroPadding,
52}
53
54///
55/// EntityNameError
56///
57
58#[derive(Debug, ThisError)]
59pub enum EntityNameError {
60    #[error("entity name is empty")]
61    Empty,
62
63    #[error("entity name length {len} exceeds max {max}")]
64    TooLong { len: usize, max: usize },
65
66    #[error("entity name must be ASCII")]
67    NonAscii,
68}
69
70///
71/// IndexNameError
72///
73
74#[derive(Debug, ThisError)]
75pub enum IndexNameError {
76    #[error("index has {len} fields (max {max})")]
77    TooManyFields { len: usize, max: usize },
78
79    #[error("index field name '{field}' exceeds max length {max}")]
80    FieldTooLong { field: String, max: usize },
81
82    #[error("index field name '{field}' must be ASCII")]
83    FieldNonAscii { field: String },
84
85    #[error("index name length {len} exceeds max {max}")]
86    TooLong { len: usize, max: usize },
87}
88
89///
90/// EntityName
91///
92
93#[derive(Clone, Copy, Eq, Hash, PartialEq)]
94pub struct EntityName {
95    len: u8,
96    bytes: [u8; MAX_ENTITY_NAME_LEN],
97}
98
99impl EntityName {
100    /// Fixed on-disk size in bytes (stable, protocol-level)
101    pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
102
103    /// Fixed in-memory size (for buffers and arrays)
104    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
105
106    /// Validate and construct an entity name from one ASCII string.
107    pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
108        // Phase 1: validate user-visible identity constraints.
109        let bytes = name.as_bytes();
110        let len = bytes.len();
111
112        if len == 0 {
113            return Err(EntityNameError::Empty);
114        }
115        if len > MAX_ENTITY_NAME_LEN {
116            return Err(EntityNameError::TooLong {
117                len,
118                max: MAX_ENTITY_NAME_LEN,
119            });
120        }
121        if !bytes.is_ascii() {
122            return Err(EntityNameError::NonAscii);
123        }
124
125        // Phase 2: write into fixed-size canonical storage.
126        let mut out = [0u8; MAX_ENTITY_NAME_LEN];
127        out[..len].copy_from_slice(bytes);
128
129        Ok(Self {
130            len: len as u8,
131            bytes: out,
132        })
133    }
134
135    /// Return the stored entity-name length.
136    #[must_use]
137    pub const fn len(&self) -> usize {
138        self.len as usize
139    }
140
141    /// Return whether the stored entity-name length is zero.
142    #[must_use]
143    pub const fn is_empty(&self) -> bool {
144        self.len() == 0
145    }
146
147    /// Borrow raw identity bytes excluding trailing fixed-buffer padding.
148    #[must_use]
149    pub fn as_bytes(&self) -> &[u8] {
150        &self.bytes[..self.len()]
151    }
152
153    /// Borrow the entity name as UTF-8 text.
154    #[must_use]
155    pub fn as_str(&self) -> &str {
156        // Invariant: construction and decoding enforce ASCII-only storage,
157        // so UTF-8 decoding cannot fail.
158        std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
159    }
160
161    /// Encode this identity into its fixed-size persisted representation.
162    #[must_use]
163    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
164        let mut out = [0u8; Self::STORED_SIZE_USIZE];
165        out[0] = self.len;
166        out[1..].copy_from_slice(&self.bytes);
167        out
168    }
169
170    /// Decode one fixed-size persisted entity identity payload.
171    pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
172        // Phase 1: validate layout and payload bounds.
173        if bytes.len() != Self::STORED_SIZE_USIZE {
174            return Err(IdentityDecodeError::InvalidSize);
175        }
176
177        let len = bytes[0] as usize;
178        if len == 0 || len > MAX_ENTITY_NAME_LEN {
179            return Err(IdentityDecodeError::InvalidLength);
180        }
181        if !bytes[1..=len].is_ascii() {
182            return Err(IdentityDecodeError::NonAscii);
183        }
184        if bytes[1 + len..].iter().any(|&b| b != 0) {
185            return Err(IdentityDecodeError::NonZeroPadding);
186        }
187
188        // Phase 2: materialize canonical fixed-buffer identity storage.
189        let mut name = [0u8; MAX_ENTITY_NAME_LEN];
190        name.copy_from_slice(&bytes[1..]);
191
192        Ok(Self {
193            len: len as u8,
194            bytes: name,
195        })
196    }
197
198    /// Return a maximal sortable entity identity sentinel value.
199    #[must_use]
200    pub const fn max_storable() -> Self {
201        Self {
202            len: MAX_ENTITY_NAME_LEN as u8,
203            bytes: [b'z'; MAX_ENTITY_NAME_LEN],
204        }
205    }
206}
207
208impl Ord for EntityName {
209    fn cmp(&self, other: &Self) -> Ordering {
210        // Keep ordering consistent with `to_bytes()` (length prefix first).
211        // This is deterministic protocol/storage ordering, not lexical string order.
212        self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
213    }
214}
215
216impl PartialOrd for EntityName {
217    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
218        Some(self.cmp(other))
219    }
220}
221
222impl Display for EntityName {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        f.write_str(self.as_str())
225    }
226}
227
228impl fmt::Debug for EntityName {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        write!(f, "EntityName({})", self.as_str())
231    }
232}
233
234///
235/// IndexName
236///
237
238#[derive(Clone, Copy, Eq, Hash, PartialEq)]
239pub struct IndexName {
240    len: u16,
241    bytes: [u8; MAX_INDEX_NAME_LEN],
242}
243
244impl IndexName {
245    /// Fixed on-disk size in bytes (stable, protocol-level).
246    pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
247    /// Fixed in-memory size (for buffers and arrays).
248    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
249
250    /// Validate and construct one index identity from an entity + field list.
251    pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
252        // Phase 1: validate index-field count and per-field identity constraints.
253        if fields.len() > MAX_INDEX_FIELDS {
254            return Err(IndexNameError::TooManyFields {
255                len: fields.len(),
256                max: MAX_INDEX_FIELDS,
257            });
258        }
259
260        let mut total_len = entity.len();
261        for field in fields {
262            let field_len = field.len();
263            if field_len > MAX_INDEX_FIELD_NAME_LEN {
264                return Err(IndexNameError::FieldTooLong {
265                    field: (*field).to_string(),
266                    max: MAX_INDEX_FIELD_NAME_LEN,
267                });
268            }
269            if !field.is_ascii() {
270                return Err(IndexNameError::FieldNonAscii {
271                    field: (*field).to_string(),
272                });
273            }
274            total_len = total_len.saturating_add(1 + field_len);
275        }
276
277        if total_len > MAX_INDEX_NAME_LEN {
278            return Err(IndexNameError::TooLong {
279                len: total_len,
280                max: MAX_INDEX_NAME_LEN,
281            });
282        }
283
284        // Phase 2: encode canonical `entity|field...` bytes into fixed storage.
285        let mut out = [0u8; MAX_INDEX_NAME_LEN];
286        let mut len = 0usize;
287
288        Self::push_bytes(&mut out, &mut len, entity.as_bytes());
289        for field in fields {
290            Self::push_bytes(&mut out, &mut len, b"|");
291            Self::push_bytes(&mut out, &mut len, field.as_bytes());
292        }
293
294        Ok(Self {
295            len: len as u16,
296            bytes: out,
297        })
298    }
299
300    /// Borrow raw index-identity bytes excluding trailing fixed-buffer padding.
301    #[must_use]
302    pub fn as_bytes(&self) -> &[u8] {
303        &self.bytes[..self.len as usize]
304    }
305
306    /// Borrow the index identity as UTF-8 text.
307    #[must_use]
308    pub fn as_str(&self) -> &str {
309        // Invariant: construction and decoding enforce ASCII-only storage,
310        // so UTF-8 decoding cannot fail.
311        std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
312    }
313
314    /// Encode this identity into its fixed-size persisted representation.
315    #[must_use]
316    pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
317        let mut out = [0u8; Self::STORED_SIZE_USIZE];
318        out[..2].copy_from_slice(&self.len.to_be_bytes());
319        out[2..].copy_from_slice(&self.bytes);
320        out
321    }
322
323    /// Decode one fixed-size persisted index identity payload.
324    pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
325        // Phase 1: validate layout and payload bounds.
326        if bytes.len() != Self::STORED_SIZE_USIZE {
327            return Err(IdentityDecodeError::InvalidSize);
328        }
329
330        let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
331        if len == 0 || len > MAX_INDEX_NAME_LEN {
332            return Err(IdentityDecodeError::InvalidLength);
333        }
334        if !bytes[2..2 + len].is_ascii() {
335            return Err(IdentityDecodeError::NonAscii);
336        }
337        if bytes[2 + len..].iter().any(|&b| b != 0) {
338            return Err(IdentityDecodeError::NonZeroPadding);
339        }
340
341        // Phase 2: materialize canonical fixed-buffer identity storage.
342        let mut name = [0u8; MAX_INDEX_NAME_LEN];
343        name.copy_from_slice(&bytes[2..]);
344
345        Ok(Self {
346            len: len as u16,
347            bytes: name,
348        })
349    }
350
351    // Append bytes into the fixed-size identity buffer while tracking write offset.
352    fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
353        let end = *len + bytes.len();
354        out[*len..end].copy_from_slice(bytes);
355        *len = end;
356    }
357
358    /// Return a maximal sortable index identity sentinel value.
359    #[must_use]
360    pub const fn max_storable() -> Self {
361        Self {
362            len: MAX_INDEX_NAME_LEN as u16,
363            bytes: [b'z'; MAX_INDEX_NAME_LEN],
364        }
365    }
366}
367
368impl Ord for IndexName {
369    fn cmp(&self, other: &Self) -> Ordering {
370        self.to_bytes().cmp(&other.to_bytes())
371    }
372}
373
374impl PartialOrd for IndexName {
375    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
376        Some(self.cmp(other))
377    }
378}
379
380impl fmt::Debug for IndexName {
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        write!(f, "IndexName({})", self.as_str())
383    }
384}
385
386impl Display for IndexName {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        f.write_str(self.as_str())
389    }
390}