icydb_core/db/identity/
mod.rs1#![expect(clippy::cast_possible_truncation)]
7#[cfg(test)]
20mod tests;
21
22use crate::MAX_INDEX_FIELDS;
23use std::{
24 cmp::Ordering,
25 fmt::{self, Display},
26};
27use thiserror::Error as ThisError;
28
29pub(super) const MAX_ENTITY_NAME_LEN: usize = 64;
34pub(super) const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
35pub(super) const MAX_INDEX_NAME_LEN: usize =
36 MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
37
38#[derive(Debug, ThisError)]
44pub enum IdentityDecodeError {
45 #[error("invalid size")]
46 InvalidSize,
47
48 #[error("invalid length")]
49 InvalidLength,
50
51 #[error("non-ascii encoding")]
52 NonAscii,
53
54 #[error("non-zero padding")]
55 NonZeroPadding,
56}
57
58#[derive(Debug, ThisError)]
63pub enum EntityNameError {
64 #[error("entity name is empty")]
65 Empty,
66
67 #[error("entity name length {len} exceeds max {max}")]
68 TooLong { len: usize, max: usize },
69
70 #[error("entity name must be ASCII")]
71 NonAscii,
72}
73
74#[derive(Debug, ThisError)]
79pub enum IndexNameError {
80 #[error("index has {len} fields (max {max})")]
81 TooManyFields { len: usize, max: usize },
82
83 #[error("index field name '{field}' exceeds max length {max}")]
84 FieldTooLong { field: String, max: usize },
85
86 #[error("index field name '{field}' must be ASCII")]
87 FieldNonAscii { field: String },
88
89 #[error("index name length {len} exceeds max {max}")]
90 TooLong { len: usize, max: usize },
91}
92
93#[derive(Clone, Copy, Eq, Hash, PartialEq)]
98pub struct EntityName {
99 len: u8,
100 bytes: [u8; MAX_ENTITY_NAME_LEN],
101}
102
103impl EntityName {
104 pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
106
107 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
109
110 pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
112 let bytes = name.as_bytes();
114 let len = bytes.len();
115
116 if len == 0 {
117 return Err(EntityNameError::Empty);
118 }
119 if len > MAX_ENTITY_NAME_LEN {
120 return Err(EntityNameError::TooLong {
121 len,
122 max: MAX_ENTITY_NAME_LEN,
123 });
124 }
125 if !bytes.is_ascii() {
126 return Err(EntityNameError::NonAscii);
127 }
128
129 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
131 out[..len].copy_from_slice(bytes);
132
133 Ok(Self {
134 len: len as u8,
135 bytes: out,
136 })
137 }
138
139 #[must_use]
141 pub const fn len(&self) -> usize {
142 self.len as usize
143 }
144
145 #[must_use]
147 pub const fn is_empty(&self) -> bool {
148 self.len() == 0
149 }
150
151 #[must_use]
153 pub fn as_bytes(&self) -> &[u8] {
154 &self.bytes[..self.len()]
155 }
156
157 #[must_use]
159 pub fn as_str(&self) -> &str {
160 std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
163 }
164
165 #[must_use]
167 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
168 let mut out = [0u8; Self::STORED_SIZE_USIZE];
169 out[0] = self.len;
170 out[1..].copy_from_slice(&self.bytes);
171 out
172 }
173
174 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
176 if bytes.len() != Self::STORED_SIZE_USIZE {
178 return Err(IdentityDecodeError::InvalidSize);
179 }
180
181 let len = bytes[0] as usize;
182 if len == 0 || len > MAX_ENTITY_NAME_LEN {
183 return Err(IdentityDecodeError::InvalidLength);
184 }
185 if !bytes[1..=len].is_ascii() {
186 return Err(IdentityDecodeError::NonAscii);
187 }
188 if bytes[1 + len..].iter().any(|&b| b != 0) {
189 return Err(IdentityDecodeError::NonZeroPadding);
190 }
191
192 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
194 name.copy_from_slice(&bytes[1..]);
195
196 Ok(Self {
197 len: len as u8,
198 bytes: name,
199 })
200 }
201
202 #[must_use]
204 pub const fn max_storable() -> Self {
205 Self {
206 len: MAX_ENTITY_NAME_LEN as u8,
207 bytes: [b'z'; MAX_ENTITY_NAME_LEN],
208 }
209 }
210}
211
212impl Ord for EntityName {
213 fn cmp(&self, other: &Self) -> Ordering {
214 self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
217 }
218}
219
220impl PartialOrd for EntityName {
221 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
222 Some(self.cmp(other))
223 }
224}
225
226impl Display for EntityName {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 f.write_str(self.as_str())
229 }
230}
231
232impl fmt::Debug for EntityName {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 write!(f, "EntityName({})", self.as_str())
235 }
236}
237
238#[derive(Clone, Copy, Eq, Hash, PartialEq)]
243pub struct IndexName {
244 len: u16,
245 bytes: [u8; MAX_INDEX_NAME_LEN],
246}
247
248impl IndexName {
249 pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
251 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
253
254 pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
256 if fields.len() > MAX_INDEX_FIELDS {
258 return Err(IndexNameError::TooManyFields {
259 len: fields.len(),
260 max: MAX_INDEX_FIELDS,
261 });
262 }
263
264 let mut total_len = entity.len();
265 for field in fields {
266 let field_len = field.len();
267 if field_len > MAX_INDEX_FIELD_NAME_LEN {
268 return Err(IndexNameError::FieldTooLong {
269 field: (*field).to_string(),
270 max: MAX_INDEX_FIELD_NAME_LEN,
271 });
272 }
273 if !field.is_ascii() {
274 return Err(IndexNameError::FieldNonAscii {
275 field: (*field).to_string(),
276 });
277 }
278 total_len = total_len.saturating_add(1 + field_len);
279 }
280
281 if total_len > MAX_INDEX_NAME_LEN {
282 return Err(IndexNameError::TooLong {
283 len: total_len,
284 max: MAX_INDEX_NAME_LEN,
285 });
286 }
287
288 let mut out = [0u8; MAX_INDEX_NAME_LEN];
290 let mut len = 0usize;
291
292 Self::push_bytes(&mut out, &mut len, entity.as_bytes());
293 for field in fields {
294 Self::push_bytes(&mut out, &mut len, b"|");
295 Self::push_bytes(&mut out, &mut len, field.as_bytes());
296 }
297
298 Ok(Self {
299 len: len as u16,
300 bytes: out,
301 })
302 }
303
304 #[must_use]
306 pub fn as_bytes(&self) -> &[u8] {
307 &self.bytes[..self.len as usize]
308 }
309
310 #[must_use]
312 pub fn as_str(&self) -> &str {
313 std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
316 }
317
318 #[must_use]
320 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
321 let mut out = [0u8; Self::STORED_SIZE_USIZE];
322 out[..2].copy_from_slice(&self.len.to_be_bytes());
323 out[2..].copy_from_slice(&self.bytes);
324 out
325 }
326
327 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
329 if bytes.len() != Self::STORED_SIZE_USIZE {
331 return Err(IdentityDecodeError::InvalidSize);
332 }
333
334 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
335 if len == 0 || len > MAX_INDEX_NAME_LEN {
336 return Err(IdentityDecodeError::InvalidLength);
337 }
338 if !bytes[2..2 + len].is_ascii() {
339 return Err(IdentityDecodeError::NonAscii);
340 }
341 if bytes[2 + len..].iter().any(|&b| b != 0) {
342 return Err(IdentityDecodeError::NonZeroPadding);
343 }
344
345 let mut name = [0u8; MAX_INDEX_NAME_LEN];
347 name.copy_from_slice(&bytes[2..]);
348
349 Ok(Self {
350 len: len as u16,
351 bytes: name,
352 })
353 }
354
355 fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
357 let end = *len + bytes.len();
358 out[*len..end].copy_from_slice(bytes);
359 *len = end;
360 }
361
362 #[must_use]
364 pub const fn max_storable() -> Self {
365 Self {
366 len: MAX_INDEX_NAME_LEN as u16,
367 bytes: [b'z'; MAX_INDEX_NAME_LEN],
368 }
369 }
370}
371
372impl Ord for IndexName {
373 fn cmp(&self, other: &Self) -> Ordering {
374 self.to_bytes().cmp(&other.to_bytes())
375 }
376}
377
378impl PartialOrd for IndexName {
379 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
380 Some(self.cmp(other))
381 }
382}
383
384impl fmt::Debug for IndexName {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 write!(f, "IndexName({})", self.as_str())
387 }
388}
389
390impl Display for IndexName {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 f.write_str(self.as_str())
393 }
394}