1#![expect(clippy::cast_possible_truncation)]
14
15#[cfg(test)]
16mod tests;
17
18use crate::MAX_INDEX_FIELDS;
19use icydb_schema::canonical_index_name_slug;
20use std::{
21 cmp::Ordering,
22 fmt::{self, Display},
23};
24
25const MAX_ENTITY_NAME_LEN: usize = 64;
30const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
31const MAX_INDEX_NAME_PREFIX_LEN: usize = 5;
32const MAX_ENTITY_NAME_SLUG_LEN: usize = (MAX_ENTITY_NAME_LEN * 3) / 2;
33const MAX_INDEX_FIELD_NAME_SLUG_LEN: usize = (MAX_INDEX_FIELD_NAME_LEN * 3) / 2;
34const MAX_INDEX_NAME_LEN: usize = MAX_INDEX_NAME_PREFIX_LEN
35 + MAX_ENTITY_NAME_SLUG_LEN
36 + 2
37 + (MAX_INDEX_FIELDS * MAX_INDEX_FIELD_NAME_SLUG_LEN)
38 + (MAX_INDEX_FIELDS - 1);
39const INDEX_NAME_SEGMENT_DELIMITER: u8 = b'|';
40
41#[derive(Debug)]
46pub enum IdentityDecodeError {
47 InvalidSize,
49
50 InvalidLength,
52
53 NonAscii,
55
56 NonZeroPadding,
58
59 Delimiter,
61}
62
63#[derive(Debug)]
68pub enum EntityNameError {
69 Empty,
71
72 TooLong { len: usize, max: usize },
74
75 NonAscii,
77
78 Delimiter,
80}
81
82#[derive(Debug)]
88pub enum IndexNameError {
89 TooManyFields { len: usize, max: usize },
91
92 NoFields,
94
95 FieldEmpty,
97
98 FieldTooLong { field: String, max: usize },
100
101 FieldNonAscii { field: String },
103
104 FieldDelimiter { field: String },
106
107 TooLong { len: usize, max: usize },
109}
110
111#[derive(Clone, Copy, Eq, Hash, PartialEq)]
116pub struct EntityName {
117 len: u8,
118 bytes: [u8; MAX_ENTITY_NAME_LEN],
119}
120
121impl EntityName {
122 pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
124
125 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
127
128 pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
130 let bytes = name.as_bytes();
132 let len = bytes.len();
133
134 if len == 0 {
135 return Err(EntityNameError::Empty);
136 }
137 if len > MAX_ENTITY_NAME_LEN {
138 return Err(EntityNameError::TooLong {
139 len,
140 max: MAX_ENTITY_NAME_LEN,
141 });
142 }
143 if !bytes.is_ascii() {
144 return Err(EntityNameError::NonAscii);
145 }
146 if bytes.contains(&INDEX_NAME_SEGMENT_DELIMITER) {
147 return Err(EntityNameError::Delimiter);
148 }
149
150 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
152 out[..len].copy_from_slice(bytes);
153
154 Ok(Self {
155 len: len as u8,
156 bytes: out,
157 })
158 }
159
160 #[must_use]
162 pub const fn len(&self) -> usize {
163 self.len as usize
164 }
165
166 #[must_use]
168 pub const fn is_empty(&self) -> bool {
169 self.len() == 0
170 }
171
172 #[must_use]
174 pub fn as_bytes(&self) -> &[u8] {
175 &self.bytes[..self.len()]
176 }
177
178 #[must_use]
181 pub fn as_str(&self) -> &str {
182 std::str::from_utf8(self.as_bytes()).unwrap_or_default()
183 }
184
185 #[must_use]
191 pub(in crate::db) fn ascii_case_fold(mut self) -> Self {
192 let len = self.len();
193 self.bytes[..len].make_ascii_lowercase();
194 self
195 }
196
197 #[must_use]
199 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
200 let mut out = [0u8; Self::STORED_SIZE_USIZE];
201 out[0] = self.len;
202 out[1..].copy_from_slice(&self.bytes);
203 out
204 }
205
206 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
208 if bytes.len() != Self::STORED_SIZE_USIZE {
210 return Err(IdentityDecodeError::InvalidSize);
211 }
212
213 let len = bytes[0] as usize;
214 if len == 0 || len > MAX_ENTITY_NAME_LEN {
215 return Err(IdentityDecodeError::InvalidLength);
216 }
217 if !bytes[1..=len].is_ascii() {
218 return Err(IdentityDecodeError::NonAscii);
219 }
220 if bytes[1..=len].contains(&INDEX_NAME_SEGMENT_DELIMITER) {
221 return Err(IdentityDecodeError::Delimiter);
222 }
223 if bytes[1 + len..].iter().any(|&b| b != 0) {
224 return Err(IdentityDecodeError::NonZeroPadding);
225 }
226
227 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
229 name.copy_from_slice(&bytes[1..]);
230
231 Ok(Self {
232 len: len as u8,
233 bytes: name,
234 })
235 }
236}
237
238impl Ord for EntityName {
239 fn cmp(&self, other: &Self) -> Ordering {
240 self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
243 }
244}
245
246impl PartialOrd for EntityName {
247 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
248 Some(self.cmp(other))
249 }
250}
251
252impl Display for EntityName {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 f.write_str(self.as_str())
255 }
256}
257
258impl fmt::Debug for EntityName {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 write!(f, "EntityName({})", self.as_str())
261 }
262}
263
264#[derive(Clone, Copy, Eq, Hash, PartialEq)]
269pub struct IndexName {
270 len: u16,
271 bytes: [u8; MAX_INDEX_NAME_LEN],
272}
273
274impl IndexName {
275 pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
277 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
279
280 pub fn try_from_entity_fields(
283 entity: &EntityName,
284 fields: &[&str],
285 ) -> Result<Self, IndexNameError> {
286 Self::try_from_entity_fields_with_prefix("idx", entity, fields)
287 }
288
289 pub fn try_unique_from_entity_fields(
292 entity: &EntityName,
293 fields: &[&str],
294 ) -> Result<Self, IndexNameError> {
295 Self::try_from_entity_fields_with_prefix("uniq", entity, fields)
296 }
297
298 fn try_from_entity_fields_with_prefix(
299 prefix: &str,
300 entity: &EntityName,
301 fields: &[&str],
302 ) -> Result<Self, IndexNameError> {
303 if fields.is_empty() {
305 return Err(IndexNameError::NoFields);
306 }
307 if fields.len() > MAX_INDEX_FIELDS {
308 return Err(IndexNameError::TooManyFields {
309 len: fields.len(),
310 max: MAX_INDEX_FIELDS,
311 });
312 }
313
314 let mut field_slugs = Vec::with_capacity(fields.len());
315 for field in fields {
316 let field_len = field.len();
317 if field_len == 0 {
318 return Err(IndexNameError::FieldEmpty);
319 }
320 if field_len > MAX_INDEX_FIELD_NAME_LEN {
321 return Err(IndexNameError::FieldTooLong {
322 field: (*field).to_string(),
323 max: MAX_INDEX_FIELD_NAME_LEN,
324 });
325 }
326 if !field.is_ascii() {
327 return Err(IndexNameError::FieldNonAscii {
328 field: (*field).to_string(),
329 });
330 }
331 if field.as_bytes().contains(&INDEX_NAME_SEGMENT_DELIMITER) {
332 return Err(IndexNameError::FieldDelimiter {
333 field: (*field).to_string(),
334 });
335 }
336 let slug = index_name_slug(field);
337 if slug.is_empty() {
338 return Err(IndexNameError::FieldEmpty);
339 }
340 field_slugs.push(slug);
341 }
342
343 let entity_slug = index_name_slug(entity.as_str());
344 let total_len = prefix
345 .len()
346 .saturating_add(1)
347 .saturating_add(entity_slug.len())
348 .saturating_add(2)
349 .saturating_add(field_slugs.iter().map(String::len).sum::<usize>())
350 .saturating_add(field_slugs.len().saturating_sub(1));
351 if total_len > MAX_INDEX_NAME_LEN {
352 return Err(IndexNameError::TooLong {
353 len: total_len,
354 max: MAX_INDEX_NAME_LEN,
355 });
356 }
357
358 let mut out = [0u8; MAX_INDEX_NAME_LEN];
360 let mut len = 0usize;
361
362 Self::push_bytes(&mut out, &mut len, prefix.as_bytes());
363 Self::push_bytes(&mut out, &mut len, b"_");
364 Self::push_bytes(&mut out, &mut len, entity_slug.as_bytes());
365 Self::push_bytes(&mut out, &mut len, b"__");
366 for (index, field_slug) in field_slugs.iter().enumerate() {
367 if index > 0 {
368 Self::push_bytes(&mut out, &mut len, b"_");
369 }
370 Self::push_bytes(&mut out, &mut len, field_slug.as_bytes());
371 }
372
373 Ok(Self {
374 len: len as u16,
375 bytes: out,
376 })
377 }
378
379 #[must_use]
381 pub fn as_bytes(&self) -> &[u8] {
382 &self.bytes[..self.len as usize]
383 }
384
385 #[must_use]
388 pub fn as_str(&self) -> &str {
389 std::str::from_utf8(self.as_bytes()).unwrap_or_default()
390 }
391
392 #[must_use]
394 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
395 let mut out = [0u8; Self::STORED_SIZE_USIZE];
396 out[..2].copy_from_slice(&self.len.to_be_bytes());
397 out[2..].copy_from_slice(&self.bytes);
398 out
399 }
400
401 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
408 if bytes.len() != Self::STORED_SIZE_USIZE {
410 return Err(IdentityDecodeError::InvalidSize);
411 }
412
413 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
414 if len == 0 || len > MAX_INDEX_NAME_LEN {
415 return Err(IdentityDecodeError::InvalidLength);
416 }
417 if !bytes[2..2 + len].is_ascii() {
418 return Err(IdentityDecodeError::NonAscii);
419 }
420 if bytes[2 + len..].iter().any(|&b| b != 0) {
421 return Err(IdentityDecodeError::NonZeroPadding);
422 }
423
424 let mut name = [0u8; MAX_INDEX_NAME_LEN];
426 name.copy_from_slice(&bytes[2..]);
427
428 Ok(Self {
429 len: len as u16,
430 bytes: name,
431 })
432 }
433
434 fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
436 let end = *len + bytes.len();
437 out[*len..end].copy_from_slice(bytes);
438 *len = end;
439 }
440}
441
442fn index_name_slug(value: &str) -> String {
443 canonical_index_name_slug(value)
444}
445
446impl Ord for IndexName {
447 fn cmp(&self, other: &Self) -> Ordering {
448 self.to_bytes().cmp(&other.to_bytes())
449 }
450}
451
452impl PartialOrd for IndexName {
453 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
454 Some(self.cmp(other))
455 }
456}
457
458impl fmt::Debug for IndexName {
459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460 write!(f, "IndexName({})", self.as_str())
461 }
462}
463
464impl Display for IndexName {
465 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466 f.write_str(self.as_str())
467 }
468}