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]
187 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
188 let mut out = [0u8; Self::STORED_SIZE_USIZE];
189 out[0] = self.len;
190 out[1..].copy_from_slice(&self.bytes);
191 out
192 }
193
194 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
196 if bytes.len() != Self::STORED_SIZE_USIZE {
198 return Err(IdentityDecodeError::InvalidSize);
199 }
200
201 let len = bytes[0] as usize;
202 if len == 0 || len > MAX_ENTITY_NAME_LEN {
203 return Err(IdentityDecodeError::InvalidLength);
204 }
205 if !bytes[1..=len].is_ascii() {
206 return Err(IdentityDecodeError::NonAscii);
207 }
208 if bytes[1..=len].contains(&INDEX_NAME_SEGMENT_DELIMITER) {
209 return Err(IdentityDecodeError::Delimiter);
210 }
211 if bytes[1 + len..].iter().any(|&b| b != 0) {
212 return Err(IdentityDecodeError::NonZeroPadding);
213 }
214
215 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
217 name.copy_from_slice(&bytes[1..]);
218
219 Ok(Self {
220 len: len as u8,
221 bytes: name,
222 })
223 }
224}
225
226impl Ord for EntityName {
227 fn cmp(&self, other: &Self) -> Ordering {
228 self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
231 }
232}
233
234impl PartialOrd for EntityName {
235 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
236 Some(self.cmp(other))
237 }
238}
239
240impl Display for EntityName {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.write_str(self.as_str())
243 }
244}
245
246impl fmt::Debug for EntityName {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 write!(f, "EntityName({})", self.as_str())
249 }
250}
251
252#[derive(Clone, Copy, Eq, Hash, PartialEq)]
257pub struct IndexName {
258 len: u16,
259 bytes: [u8; MAX_INDEX_NAME_LEN],
260}
261
262impl IndexName {
263 pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
265 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
267
268 pub fn try_from_entity_fields(
271 entity: &EntityName,
272 fields: &[&str],
273 ) -> Result<Self, IndexNameError> {
274 Self::try_from_entity_fields_with_prefix("idx", entity, fields)
275 }
276
277 pub fn try_unique_from_entity_fields(
280 entity: &EntityName,
281 fields: &[&str],
282 ) -> Result<Self, IndexNameError> {
283 Self::try_from_entity_fields_with_prefix("uniq", entity, fields)
284 }
285
286 fn try_from_entity_fields_with_prefix(
287 prefix: &str,
288 entity: &EntityName,
289 fields: &[&str],
290 ) -> Result<Self, IndexNameError> {
291 if fields.is_empty() {
293 return Err(IndexNameError::NoFields);
294 }
295 if fields.len() > MAX_INDEX_FIELDS {
296 return Err(IndexNameError::TooManyFields {
297 len: fields.len(),
298 max: MAX_INDEX_FIELDS,
299 });
300 }
301
302 let mut field_slugs = Vec::with_capacity(fields.len());
303 for field in fields {
304 let field_len = field.len();
305 if field_len == 0 {
306 return Err(IndexNameError::FieldEmpty);
307 }
308 if field_len > MAX_INDEX_FIELD_NAME_LEN {
309 return Err(IndexNameError::FieldTooLong {
310 field: (*field).to_string(),
311 max: MAX_INDEX_FIELD_NAME_LEN,
312 });
313 }
314 if !field.is_ascii() {
315 return Err(IndexNameError::FieldNonAscii {
316 field: (*field).to_string(),
317 });
318 }
319 if field.as_bytes().contains(&INDEX_NAME_SEGMENT_DELIMITER) {
320 return Err(IndexNameError::FieldDelimiter {
321 field: (*field).to_string(),
322 });
323 }
324 let slug = index_name_slug(field);
325 if slug.is_empty() {
326 return Err(IndexNameError::FieldEmpty);
327 }
328 field_slugs.push(slug);
329 }
330
331 let entity_slug = index_name_slug(entity.as_str());
332 let total_len = prefix
333 .len()
334 .saturating_add(1)
335 .saturating_add(entity_slug.len())
336 .saturating_add(2)
337 .saturating_add(field_slugs.iter().map(String::len).sum::<usize>())
338 .saturating_add(field_slugs.len().saturating_sub(1));
339 if total_len > MAX_INDEX_NAME_LEN {
340 return Err(IndexNameError::TooLong {
341 len: total_len,
342 max: MAX_INDEX_NAME_LEN,
343 });
344 }
345
346 let mut out = [0u8; MAX_INDEX_NAME_LEN];
348 let mut len = 0usize;
349
350 Self::push_bytes(&mut out, &mut len, prefix.as_bytes());
351 Self::push_bytes(&mut out, &mut len, b"_");
352 Self::push_bytes(&mut out, &mut len, entity_slug.as_bytes());
353 Self::push_bytes(&mut out, &mut len, b"__");
354 for (index, field_slug) in field_slugs.iter().enumerate() {
355 if index > 0 {
356 Self::push_bytes(&mut out, &mut len, b"_");
357 }
358 Self::push_bytes(&mut out, &mut len, field_slug.as_bytes());
359 }
360
361 Ok(Self {
362 len: len as u16,
363 bytes: out,
364 })
365 }
366
367 #[must_use]
369 pub fn as_bytes(&self) -> &[u8] {
370 &self.bytes[..self.len as usize]
371 }
372
373 #[must_use]
376 pub fn as_str(&self) -> &str {
377 std::str::from_utf8(self.as_bytes()).unwrap_or_default()
378 }
379
380 #[must_use]
382 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
383 let mut out = [0u8; Self::STORED_SIZE_USIZE];
384 out[..2].copy_from_slice(&self.len.to_be_bytes());
385 out[2..].copy_from_slice(&self.bytes);
386 out
387 }
388
389 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
396 if bytes.len() != Self::STORED_SIZE_USIZE {
398 return Err(IdentityDecodeError::InvalidSize);
399 }
400
401 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
402 if len == 0 || len > MAX_INDEX_NAME_LEN {
403 return Err(IdentityDecodeError::InvalidLength);
404 }
405 if !bytes[2..2 + len].is_ascii() {
406 return Err(IdentityDecodeError::NonAscii);
407 }
408 if bytes[2 + len..].iter().any(|&b| b != 0) {
409 return Err(IdentityDecodeError::NonZeroPadding);
410 }
411
412 let mut name = [0u8; MAX_INDEX_NAME_LEN];
414 name.copy_from_slice(&bytes[2..]);
415
416 Ok(Self {
417 len: len as u16,
418 bytes: name,
419 })
420 }
421
422 fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
424 let end = *len + bytes.len();
425 out[*len..end].copy_from_slice(bytes);
426 *len = end;
427 }
428}
429
430fn index_name_slug(value: &str) -> String {
431 canonical_index_name_slug(value)
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}