Skip to main content

icydb_core/db/identity/
mod.rs

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