Skip to main content

icydb_core/db/identity/
mod.rs

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