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