icydb_core/db/identity/
mod.rs1#![expect(clippy::cast_possible_truncation)]
14
15#[cfg(test)]
16mod tests;
17
18use crate::MAX_INDEX_FIELDS;
19use std::{
20 cmp::Ordering,
21 fmt::{self, Display},
22};
23use thiserror::Error as ThisError;
24
25pub(super) const MAX_ENTITY_NAME_LEN: usize = 64;
30pub(super) const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
31pub(super) const MAX_INDEX_NAME_LEN: usize =
32 MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
33
34#[derive(Debug, ThisError)]
40pub enum IdentityDecodeError {
41 #[error("invalid size")]
42 InvalidSize,
43
44 #[error("invalid length")]
45 InvalidLength,
46
47 #[error("non-ascii encoding")]
48 NonAscii,
49
50 #[error("non-zero padding")]
51 NonZeroPadding,
52}
53
54#[derive(Debug, ThisError)]
59pub enum EntityNameError {
60 #[error("entity name is empty")]
61 Empty,
62
63 #[error("entity name length {len} exceeds max {max}")]
64 TooLong { len: usize, max: usize },
65
66 #[error("entity name must be ASCII")]
67 NonAscii,
68}
69
70#[derive(Debug, ThisError)]
75pub enum IndexNameError {
76 #[error("index has {len} fields (max {max})")]
77 TooManyFields { len: usize, max: usize },
78
79 #[error("index field name '{field}' exceeds max length {max}")]
80 FieldTooLong { field: String, max: usize },
81
82 #[error("index field name '{field}' must be ASCII")]
83 FieldNonAscii { field: String },
84
85 #[error("index name length {len} exceeds max {max}")]
86 TooLong { len: usize, max: usize },
87}
88
89#[derive(Clone, Copy, Eq, Hash, PartialEq)]
94pub struct EntityName {
95 len: u8,
96 bytes: [u8; MAX_ENTITY_NAME_LEN],
97}
98
99impl EntityName {
100 pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
102
103 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
105
106 pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
108 let bytes = name.as_bytes();
110 let len = bytes.len();
111
112 if len == 0 {
113 return Err(EntityNameError::Empty);
114 }
115 if len > MAX_ENTITY_NAME_LEN {
116 return Err(EntityNameError::TooLong {
117 len,
118 max: MAX_ENTITY_NAME_LEN,
119 });
120 }
121 if !bytes.is_ascii() {
122 return Err(EntityNameError::NonAscii);
123 }
124
125 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
127 out[..len].copy_from_slice(bytes);
128
129 Ok(Self {
130 len: len as u8,
131 bytes: out,
132 })
133 }
134
135 #[must_use]
137 pub const fn len(&self) -> usize {
138 self.len as usize
139 }
140
141 #[must_use]
143 pub const fn is_empty(&self) -> bool {
144 self.len() == 0
145 }
146
147 #[must_use]
149 pub fn as_bytes(&self) -> &[u8] {
150 &self.bytes[..self.len()]
151 }
152
153 #[must_use]
155 pub fn as_str(&self) -> &str {
156 std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
159 }
160
161 #[must_use]
163 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
164 let mut out = [0u8; Self::STORED_SIZE_USIZE];
165 out[0] = self.len;
166 out[1..].copy_from_slice(&self.bytes);
167 out
168 }
169
170 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
172 if bytes.len() != Self::STORED_SIZE_USIZE {
174 return Err(IdentityDecodeError::InvalidSize);
175 }
176
177 let len = bytes[0] as usize;
178 if len == 0 || len > MAX_ENTITY_NAME_LEN {
179 return Err(IdentityDecodeError::InvalidLength);
180 }
181 if !bytes[1..=len].is_ascii() {
182 return Err(IdentityDecodeError::NonAscii);
183 }
184 if bytes[1 + len..].iter().any(|&b| b != 0) {
185 return Err(IdentityDecodeError::NonZeroPadding);
186 }
187
188 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
190 name.copy_from_slice(&bytes[1..]);
191
192 Ok(Self {
193 len: len as u8,
194 bytes: name,
195 })
196 }
197
198 #[must_use]
200 pub const fn max_storable() -> Self {
201 Self {
202 len: MAX_ENTITY_NAME_LEN as u8,
203 bytes: [b'z'; MAX_ENTITY_NAME_LEN],
204 }
205 }
206}
207
208impl Ord for EntityName {
209 fn cmp(&self, other: &Self) -> Ordering {
210 self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
213 }
214}
215
216impl PartialOrd for EntityName {
217 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
218 Some(self.cmp(other))
219 }
220}
221
222impl Display for EntityName {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 f.write_str(self.as_str())
225 }
226}
227
228impl fmt::Debug for EntityName {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 write!(f, "EntityName({})", self.as_str())
231 }
232}
233
234#[derive(Clone, Copy, Eq, Hash, PartialEq)]
239pub struct IndexName {
240 len: u16,
241 bytes: [u8; MAX_INDEX_NAME_LEN],
242}
243
244impl IndexName {
245 pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
247 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
249
250 pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
252 if fields.len() > MAX_INDEX_FIELDS {
254 return Err(IndexNameError::TooManyFields {
255 len: fields.len(),
256 max: MAX_INDEX_FIELDS,
257 });
258 }
259
260 let mut total_len = entity.len();
261 for field in fields {
262 let field_len = field.len();
263 if field_len > MAX_INDEX_FIELD_NAME_LEN {
264 return Err(IndexNameError::FieldTooLong {
265 field: (*field).to_string(),
266 max: MAX_INDEX_FIELD_NAME_LEN,
267 });
268 }
269 if !field.is_ascii() {
270 return Err(IndexNameError::FieldNonAscii {
271 field: (*field).to_string(),
272 });
273 }
274 total_len = total_len.saturating_add(1 + field_len);
275 }
276
277 if total_len > MAX_INDEX_NAME_LEN {
278 return Err(IndexNameError::TooLong {
279 len: total_len,
280 max: MAX_INDEX_NAME_LEN,
281 });
282 }
283
284 let mut out = [0u8; MAX_INDEX_NAME_LEN];
286 let mut len = 0usize;
287
288 Self::push_bytes(&mut out, &mut len, entity.as_bytes());
289 for field in fields {
290 Self::push_bytes(&mut out, &mut len, b"|");
291 Self::push_bytes(&mut out, &mut len, field.as_bytes());
292 }
293
294 Ok(Self {
295 len: len as u16,
296 bytes: out,
297 })
298 }
299
300 #[must_use]
302 pub fn as_bytes(&self) -> &[u8] {
303 &self.bytes[..self.len as usize]
304 }
305
306 #[must_use]
308 pub fn as_str(&self) -> &str {
309 std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
312 }
313
314 #[must_use]
316 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
317 let mut out = [0u8; Self::STORED_SIZE_USIZE];
318 out[..2].copy_from_slice(&self.len.to_be_bytes());
319 out[2..].copy_from_slice(&self.bytes);
320 out
321 }
322
323 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
325 if bytes.len() != Self::STORED_SIZE_USIZE {
327 return Err(IdentityDecodeError::InvalidSize);
328 }
329
330 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
331 if len == 0 || len > MAX_INDEX_NAME_LEN {
332 return Err(IdentityDecodeError::InvalidLength);
333 }
334 if !bytes[2..2 + len].is_ascii() {
335 return Err(IdentityDecodeError::NonAscii);
336 }
337 if bytes[2 + len..].iter().any(|&b| b != 0) {
338 return Err(IdentityDecodeError::NonZeroPadding);
339 }
340
341 let mut name = [0u8; MAX_INDEX_NAME_LEN];
343 name.copy_from_slice(&bytes[2..]);
344
345 Ok(Self {
346 len: len as u16,
347 bytes: name,
348 })
349 }
350
351 fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
353 let end = *len + bytes.len();
354 out[*len..end].copy_from_slice(bytes);
355 *len = end;
356 }
357
358 #[must_use]
360 pub const fn max_storable() -> Self {
361 Self {
362 len: MAX_INDEX_NAME_LEN as u16,
363 bytes: [b'z'; MAX_INDEX_NAME_LEN],
364 }
365 }
366}
367
368impl Ord for IndexName {
369 fn cmp(&self, other: &Self) -> Ordering {
370 self.to_bytes().cmp(&other.to_bytes())
371 }
372}
373
374impl PartialOrd for IndexName {
375 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
376 Some(self.cmp(other))
377 }
378}
379
380impl fmt::Debug for IndexName {
381 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382 write!(f, "IndexName({})", self.as_str())
383 }
384}
385
386impl Display for IndexName {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 f.write_str(self.as_str())
389 }
390}