icydb_core/db/identity/
mod.rs1#![expect(clippy::cast_possible_truncation)]
2#[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
24pub(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#[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#[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#[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#[derive(Clone, Copy, Eq, Hash, PartialEq)]
93pub struct EntityName {
94 len: u8,
95 bytes: [u8; MAX_ENTITY_NAME_LEN],
96}
97
98impl EntityName {
99 pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
101
102 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
104
105 pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
107 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 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 pub const fn len(&self) -> usize {
137 self.len as usize
138 }
139
140 #[must_use]
141 pub const fn is_empty(&self) -> bool {
143 self.len() == 0
144 }
145
146 #[must_use]
147 pub fn as_bytes(&self) -> &[u8] {
149 &self.bytes[..self.len()]
150 }
151
152 #[must_use]
153 pub fn as_str(&self) -> &str {
155 std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
158 }
159
160 #[must_use]
161 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 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
171 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 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 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 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#[derive(Clone, Copy, Eq, Hash, PartialEq)]
238pub struct IndexName {
239 len: u16,
240 bytes: [u8; MAX_INDEX_NAME_LEN],
241}
242
243impl IndexName {
244 pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
246 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
248
249 pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
251 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 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 pub fn as_bytes(&self) -> &[u8] {
302 &self.bytes[..self.len as usize]
303 }
304
305 #[must_use]
306 pub fn as_str(&self) -> &str {
308 std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
311 }
312
313 #[must_use]
314 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 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
324 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 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 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 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}