1#![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));
33const INDEX_NAME_SEGMENT_DELIMITER: u8 = b'|';
34const MAX_ASCII_BYTE: u8 = 0x7F;
35
36#[derive(Debug, ThisError)]
42pub enum IdentityDecodeError {
43 #[error("invalid size")]
44 InvalidSize,
45
46 #[error("invalid length")]
47 InvalidLength,
48
49 #[error("non-ascii encoding")]
50 NonAscii,
51
52 #[error("non-zero padding")]
53 NonZeroPadding,
54
55 #[error("reserved identity delimiter")]
56 Delimiter,
57}
58
59#[derive(Debug, ThisError)]
64pub enum EntityNameError {
65 #[error("entity name is empty")]
66 Empty,
67
68 #[error("entity name length {len} exceeds max {max}")]
69 TooLong { len: usize, max: usize },
70
71 #[error("entity name must be ASCII")]
72 NonAscii,
73
74 #[error("entity name must not contain '|'")]
75 Delimiter,
76}
77
78#[derive(Debug, ThisError)]
83pub enum IndexNameError {
84 #[error("index has {len} fields (max {max})")]
85 TooManyFields { len: usize, max: usize },
86
87 #[error("index must reference at least one field")]
88 NoFields,
89
90 #[error("index field name is empty")]
91 FieldEmpty,
92
93 #[error("index field name '{field}' exceeds max length {max}")]
94 FieldTooLong { field: String, max: usize },
95
96 #[error("index field name '{field}' must be ASCII")]
97 FieldNonAscii { field: String },
98
99 #[error("index field name '{field}' must not contain '|'")]
100 FieldDelimiter { field: String },
101
102 #[error("index name length {len} exceeds max {max}")]
103 TooLong { len: usize, max: usize },
104}
105
106#[derive(Clone, Copy, Eq, Hash, PartialEq)]
111pub struct EntityName {
112 len: u8,
113 bytes: [u8; MAX_ENTITY_NAME_LEN],
114}
115
116impl EntityName {
117 pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
119
120 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
122
123 pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
125 let bytes = name.as_bytes();
127 let len = bytes.len();
128
129 if len == 0 {
130 return Err(EntityNameError::Empty);
131 }
132 if len > MAX_ENTITY_NAME_LEN {
133 return Err(EntityNameError::TooLong {
134 len,
135 max: MAX_ENTITY_NAME_LEN,
136 });
137 }
138 if !bytes.is_ascii() {
139 return Err(EntityNameError::NonAscii);
140 }
141 if bytes.contains(&INDEX_NAME_SEGMENT_DELIMITER) {
142 return Err(EntityNameError::Delimiter);
143 }
144
145 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
147 out[..len].copy_from_slice(bytes);
148
149 Ok(Self {
150 len: len as u8,
151 bytes: out,
152 })
153 }
154
155 #[must_use]
157 pub const fn len(&self) -> usize {
158 self.len as usize
159 }
160
161 #[must_use]
163 pub const fn is_empty(&self) -> bool {
164 self.len() == 0
165 }
166
167 #[must_use]
169 pub fn as_bytes(&self) -> &[u8] {
170 &self.bytes[..self.len()]
171 }
172
173 #[must_use]
175 pub fn as_str(&self) -> &str {
176 std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
179 }
180
181 #[must_use]
183 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
184 let mut out = [0u8; Self::STORED_SIZE_USIZE];
185 out[0] = self.len;
186 out[1..].copy_from_slice(&self.bytes);
187 out
188 }
189
190 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
192 if bytes.len() != Self::STORED_SIZE_USIZE {
194 return Err(IdentityDecodeError::InvalidSize);
195 }
196
197 let len = bytes[0] as usize;
198 if len == 0 || len > MAX_ENTITY_NAME_LEN {
199 return Err(IdentityDecodeError::InvalidLength);
200 }
201 if !bytes[1..=len].is_ascii() {
202 return Err(IdentityDecodeError::NonAscii);
203 }
204 if bytes[1..=len].contains(&INDEX_NAME_SEGMENT_DELIMITER) {
205 return Err(IdentityDecodeError::Delimiter);
206 }
207 if bytes[1 + len..].iter().any(|&b| b != 0) {
208 return Err(IdentityDecodeError::NonZeroPadding);
209 }
210
211 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
213 name.copy_from_slice(&bytes[1..]);
214
215 Ok(Self {
216 len: len as u8,
217 bytes: name,
218 })
219 }
220
221 #[must_use]
223 pub const fn max_storable() -> Self {
224 Self {
225 len: MAX_ENTITY_NAME_LEN as u8,
226 bytes: [MAX_ASCII_BYTE; MAX_ENTITY_NAME_LEN],
227 }
228 }
229}
230
231impl Ord for EntityName {
232 fn cmp(&self, other: &Self) -> Ordering {
233 self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
236 }
237}
238
239impl PartialOrd for EntityName {
240 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
241 Some(self.cmp(other))
242 }
243}
244
245impl Display for EntityName {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 f.write_str(self.as_str())
248 }
249}
250
251impl fmt::Debug for EntityName {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 write!(f, "EntityName({})", self.as_str())
254 }
255}
256
257#[derive(Clone, Copy, Eq, Hash, PartialEq)]
262pub struct IndexName {
263 len: u16,
264 bytes: [u8; MAX_INDEX_NAME_LEN],
265}
266
267impl IndexName {
268 pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
270 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
272
273 pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
275 if fields.is_empty() {
277 return Err(IndexNameError::NoFields);
278 }
279 if fields.len() > MAX_INDEX_FIELDS {
280 return Err(IndexNameError::TooManyFields {
281 len: fields.len(),
282 max: MAX_INDEX_FIELDS,
283 });
284 }
285
286 let mut total_len = entity.len();
287 for field in fields {
288 let field_len = field.len();
289 if field_len == 0 {
290 return Err(IndexNameError::FieldEmpty);
291 }
292 if field_len > MAX_INDEX_FIELD_NAME_LEN {
293 return Err(IndexNameError::FieldTooLong {
294 field: (*field).to_string(),
295 max: MAX_INDEX_FIELD_NAME_LEN,
296 });
297 }
298 if !field.is_ascii() {
299 return Err(IndexNameError::FieldNonAscii {
300 field: (*field).to_string(),
301 });
302 }
303 if field.as_bytes().contains(&INDEX_NAME_SEGMENT_DELIMITER) {
304 return Err(IndexNameError::FieldDelimiter {
305 field: (*field).to_string(),
306 });
307 }
308 total_len = total_len.saturating_add(1 + field_len);
309 }
310
311 if total_len > MAX_INDEX_NAME_LEN {
312 return Err(IndexNameError::TooLong {
313 len: total_len,
314 max: MAX_INDEX_NAME_LEN,
315 });
316 }
317
318 let mut out = [0u8; MAX_INDEX_NAME_LEN];
320 let mut len = 0usize;
321
322 Self::push_bytes(&mut out, &mut len, entity.as_bytes());
323 for field in fields {
324 Self::push_bytes(&mut out, &mut len, b"|");
325 Self::push_bytes(&mut out, &mut len, field.as_bytes());
326 }
327
328 Ok(Self {
329 len: len as u16,
330 bytes: out,
331 })
332 }
333
334 #[must_use]
336 pub fn as_bytes(&self) -> &[u8] {
337 &self.bytes[..self.len as usize]
338 }
339
340 #[must_use]
342 pub fn as_str(&self) -> &str {
343 std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
346 }
347
348 #[must_use]
350 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
351 let mut out = [0u8; Self::STORED_SIZE_USIZE];
352 out[..2].copy_from_slice(&self.len.to_be_bytes());
353 out[2..].copy_from_slice(&self.bytes);
354 out
355 }
356
357 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
364 if bytes.len() != Self::STORED_SIZE_USIZE {
366 return Err(IdentityDecodeError::InvalidSize);
367 }
368
369 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
370 if len == 0 || len > MAX_INDEX_NAME_LEN {
371 return Err(IdentityDecodeError::InvalidLength);
372 }
373 if !bytes[2..2 + len].is_ascii() {
374 return Err(IdentityDecodeError::NonAscii);
375 }
376 if bytes[2 + len..].iter().any(|&b| b != 0) {
377 return Err(IdentityDecodeError::NonZeroPadding);
378 }
379
380 let mut name = [0u8; MAX_INDEX_NAME_LEN];
382 name.copy_from_slice(&bytes[2..]);
383
384 Ok(Self {
385 len: len as u16,
386 bytes: name,
387 })
388 }
389
390 fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
392 let end = *len + bytes.len();
393 out[*len..end].copy_from_slice(bytes);
394 *len = end;
395 }
396
397 #[must_use]
399 pub const fn max_storable() -> Self {
400 Self {
401 len: MAX_INDEX_NAME_LEN as u16,
402 bytes: [MAX_ASCII_BYTE; MAX_INDEX_NAME_LEN],
403 }
404 }
405}
406
407impl Ord for IndexName {
408 fn cmp(&self, other: &Self) -> Ordering {
409 self.to_bytes().cmp(&other.to_bytes())
410 }
411}
412
413impl PartialOrd for IndexName {
414 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
415 Some(self.cmp(other))
416 }
417}
418
419impl fmt::Debug for IndexName {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 write!(f, "IndexName({})", self.as_str())
422 }
423}
424
425impl Display for IndexName {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 f.write_str(self.as_str())
428 }
429}