1#![expect(clippy::cast_possible_truncation)]
14
15#[cfg(test)]
16mod tests;
17
18use crate::MAX_INDEX_FIELDS;
19use icydb_utils::to_snake_case;
20use std::{
21 cmp::Ordering,
22 fmt::{self, Display},
23};
24
25pub(super) const MAX_ENTITY_NAME_LEN: usize = 64;
30pub(super) const 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;
34pub(super) const 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#[derive(Debug)]
46pub enum IdentityDecodeError {
47 InvalidSize,
48
49 InvalidLength,
50
51 NonAscii,
52
53 NonZeroPadding,
54
55 Delimiter,
56}
57
58#[derive(Debug)]
63pub enum EntityNameError {
64 Empty,
65
66 TooLong { len: usize, max: usize },
67
68 NonAscii,
69
70 Delimiter,
71}
72
73#[derive(Debug)]
78pub enum IndexNameError {
79 TooManyFields { len: usize, max: usize },
80
81 NoFields,
82
83 FieldEmpty,
84
85 FieldTooLong { field: String, max: usize },
86
87 FieldNonAscii { field: String },
88
89 FieldDelimiter { field: String },
90
91 TooLong { len: usize, max: usize },
92}
93
94#[derive(Clone, Copy, Eq, Hash, PartialEq)]
99pub struct EntityName {
100 len: u8,
101 bytes: [u8; MAX_ENTITY_NAME_LEN],
102}
103
104impl EntityName {
105 pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
107
108 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
110
111 pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
113 let bytes = name.as_bytes();
115 let len = bytes.len();
116
117 if len == 0 {
118 return Err(EntityNameError::Empty);
119 }
120 if len > MAX_ENTITY_NAME_LEN {
121 return Err(EntityNameError::TooLong {
122 len,
123 max: MAX_ENTITY_NAME_LEN,
124 });
125 }
126 if !bytes.is_ascii() {
127 return Err(EntityNameError::NonAscii);
128 }
129 if bytes.contains(&INDEX_NAME_SEGMENT_DELIMITER) {
130 return Err(EntityNameError::Delimiter);
131 }
132
133 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
135 out[..len].copy_from_slice(bytes);
136
137 Ok(Self {
138 len: len as u8,
139 bytes: out,
140 })
141 }
142
143 #[must_use]
145 pub const fn len(&self) -> usize {
146 self.len as usize
147 }
148
149 #[must_use]
151 pub const fn is_empty(&self) -> bool {
152 self.len() == 0
153 }
154
155 #[must_use]
157 pub fn as_bytes(&self) -> &[u8] {
158 &self.bytes[..self.len()]
159 }
160
161 #[must_use]
168 pub fn as_str(&self) -> &str {
169 std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
172 }
173
174 #[must_use]
176 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
177 let mut out = [0u8; Self::STORED_SIZE_USIZE];
178 out[0] = self.len;
179 out[1..].copy_from_slice(&self.bytes);
180 out
181 }
182
183 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
185 if bytes.len() != Self::STORED_SIZE_USIZE {
187 return Err(IdentityDecodeError::InvalidSize);
188 }
189
190 let len = bytes[0] as usize;
191 if len == 0 || len > MAX_ENTITY_NAME_LEN {
192 return Err(IdentityDecodeError::InvalidLength);
193 }
194 if !bytes[1..=len].is_ascii() {
195 return Err(IdentityDecodeError::NonAscii);
196 }
197 if bytes[1..=len].contains(&INDEX_NAME_SEGMENT_DELIMITER) {
198 return Err(IdentityDecodeError::Delimiter);
199 }
200 if bytes[1 + len..].iter().any(|&b| b != 0) {
201 return Err(IdentityDecodeError::NonZeroPadding);
202 }
203
204 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
206 name.copy_from_slice(&bytes[1..]);
207
208 Ok(Self {
209 len: len as u8,
210 bytes: name,
211 })
212 }
213}
214
215impl Ord for EntityName {
216 fn cmp(&self, other: &Self) -> Ordering {
217 self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
220 }
221}
222
223impl PartialOrd for EntityName {
224 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
225 Some(self.cmp(other))
226 }
227}
228
229impl Display for EntityName {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 f.write_str(self.as_str())
232 }
233}
234
235impl fmt::Debug for EntityName {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 write!(f, "EntityName({})", self.as_str())
238 }
239}
240
241#[derive(Clone, Copy, Eq, Hash, PartialEq)]
246pub struct IndexName {
247 len: u16,
248 bytes: [u8; MAX_INDEX_NAME_LEN],
249}
250
251impl IndexName {
252 pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
254 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
256
257 pub fn try_from_entity_fields(
260 entity: &EntityName,
261 fields: &[&str],
262 ) -> Result<Self, IndexNameError> {
263 Self::try_from_entity_fields_with_prefix("idx", entity, fields)
264 }
265
266 pub fn try_unique_from_entity_fields(
269 entity: &EntityName,
270 fields: &[&str],
271 ) -> Result<Self, IndexNameError> {
272 Self::try_from_entity_fields_with_prefix("uniq", entity, fields)
273 }
274
275 fn try_from_entity_fields_with_prefix(
276 prefix: &str,
277 entity: &EntityName,
278 fields: &[&str],
279 ) -> Result<Self, IndexNameError> {
280 if fields.is_empty() {
282 return Err(IndexNameError::NoFields);
283 }
284 if fields.len() > MAX_INDEX_FIELDS {
285 return Err(IndexNameError::TooManyFields {
286 len: fields.len(),
287 max: MAX_INDEX_FIELDS,
288 });
289 }
290
291 let mut field_slugs = Vec::with_capacity(fields.len());
292 for field in fields {
293 let field_len = field.len();
294 if field_len == 0 {
295 return Err(IndexNameError::FieldEmpty);
296 }
297 if field_len > MAX_INDEX_FIELD_NAME_LEN {
298 return Err(IndexNameError::FieldTooLong {
299 field: (*field).to_string(),
300 max: MAX_INDEX_FIELD_NAME_LEN,
301 });
302 }
303 if !field.is_ascii() {
304 return Err(IndexNameError::FieldNonAscii {
305 field: (*field).to_string(),
306 });
307 }
308 if field.as_bytes().contains(&INDEX_NAME_SEGMENT_DELIMITER) {
309 return Err(IndexNameError::FieldDelimiter {
310 field: (*field).to_string(),
311 });
312 }
313 let slug = index_name_slug(field);
314 if slug.is_empty() {
315 return Err(IndexNameError::FieldEmpty);
316 }
317 field_slugs.push(slug);
318 }
319
320 let entity_slug = index_name_slug(entity.as_str());
321 let total_len = prefix
322 .len()
323 .saturating_add(1)
324 .saturating_add(entity_slug.len())
325 .saturating_add(2)
326 .saturating_add(field_slugs.iter().map(String::len).sum::<usize>())
327 .saturating_add(field_slugs.len().saturating_sub(1));
328 if total_len > MAX_INDEX_NAME_LEN {
329 return Err(IndexNameError::TooLong {
330 len: total_len,
331 max: MAX_INDEX_NAME_LEN,
332 });
333 }
334
335 let mut out = [0u8; MAX_INDEX_NAME_LEN];
337 let mut len = 0usize;
338
339 Self::push_bytes(&mut out, &mut len, prefix.as_bytes());
340 Self::push_bytes(&mut out, &mut len, b"_");
341 Self::push_bytes(&mut out, &mut len, entity_slug.as_bytes());
342 Self::push_bytes(&mut out, &mut len, b"__");
343 for (index, field_slug) in field_slugs.iter().enumerate() {
344 if index > 0 {
345 Self::push_bytes(&mut out, &mut len, b"_");
346 }
347 Self::push_bytes(&mut out, &mut len, field_slug.as_bytes());
348 }
349
350 Ok(Self {
351 len: len as u16,
352 bytes: out,
353 })
354 }
355
356 #[must_use]
358 pub fn as_bytes(&self) -> &[u8] {
359 &self.bytes[..self.len as usize]
360 }
361
362 #[must_use]
369 pub fn as_str(&self) -> &str {
370 std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
373 }
374
375 #[must_use]
377 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
378 let mut out = [0u8; Self::STORED_SIZE_USIZE];
379 out[..2].copy_from_slice(&self.len.to_be_bytes());
380 out[2..].copy_from_slice(&self.bytes);
381 out
382 }
383
384 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
391 if bytes.len() != Self::STORED_SIZE_USIZE {
393 return Err(IdentityDecodeError::InvalidSize);
394 }
395
396 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
397 if len == 0 || len > MAX_INDEX_NAME_LEN {
398 return Err(IdentityDecodeError::InvalidLength);
399 }
400 if !bytes[2..2 + len].is_ascii() {
401 return Err(IdentityDecodeError::NonAscii);
402 }
403 if bytes[2 + len..].iter().any(|&b| b != 0) {
404 return Err(IdentityDecodeError::NonZeroPadding);
405 }
406
407 let mut name = [0u8; MAX_INDEX_NAME_LEN];
409 name.copy_from_slice(&bytes[2..]);
410
411 Ok(Self {
412 len: len as u16,
413 bytes: name,
414 })
415 }
416
417 fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
419 let end = *len + bytes.len();
420 out[*len..end].copy_from_slice(bytes);
421 *len = end;
422 }
423}
424
425fn index_name_slug(value: &str) -> String {
426 let separated = value
427 .chars()
428 .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
429 .collect::<String>();
430
431 to_snake_case(separated.as_str())
432}
433
434impl Ord for IndexName {
435 fn cmp(&self, other: &Self) -> Ordering {
436 self.to_bytes().cmp(&other.to_bytes())
437 }
438}
439
440impl PartialOrd for IndexName {
441 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
442 Some(self.cmp(other))
443 }
444}
445
446impl fmt::Debug for IndexName {
447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448 write!(f, "IndexName({})", self.as_str())
449 }
450}
451
452impl Display for IndexName {
453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454 f.write_str(self.as_str())
455 }
456}