Skip to main content

icydb_core/db/identity/
mod.rs

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