1#![allow(clippy::cast_possible_truncation)]
2
3use crate::MAX_INDEX_FIELDS;
4use std::{
5 cmp::Ordering,
6 fmt::{self, Display},
7};
8
9pub const MAX_ENTITY_NAME_LEN: usize = 64;
14pub const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
15pub const MAX_INDEX_NAME_LEN: usize =
16 MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
17
18#[derive(Clone, Copy, Eq, Hash, PartialEq)]
23pub struct EntityName {
24 pub len: u8,
25 pub bytes: [u8; MAX_ENTITY_NAME_LEN],
26}
27
28impl EntityName {
29 pub const STORED_SIZE: u32 = 1 + MAX_ENTITY_NAME_LEN as u32;
30 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
31
32 #[must_use]
33 pub const fn from_static(name: &'static str) -> Self {
34 let bytes = name.as_bytes();
35 let len = bytes.len();
36
37 assert!(
38 len > 0 && len <= MAX_ENTITY_NAME_LEN,
39 "entity name length out of bounds"
40 );
41
42 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
43 let mut i = 0;
44 while i < len {
45 let b = bytes[i];
46 assert!(b.is_ascii(), "entity name must be ASCII");
47 out[i] = b;
48 i += 1;
49 }
50
51 Self {
52 len: len as u8,
53 bytes: out,
54 }
55 }
56
57 #[must_use]
58 pub const fn len(&self) -> usize {
59 self.len as usize
60 }
61
62 #[must_use]
63 pub const fn is_empty(&self) -> bool {
64 self.len == 0
65 }
66
67 #[must_use]
68 pub fn as_bytes(&self) -> &[u8] {
69 &self.bytes[..self.len()]
70 }
71
72 #[must_use]
73 pub fn as_str(&self) -> &str {
74 unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
76 }
77
78 #[must_use]
79 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
80 let mut out = [0u8; Self::STORED_SIZE_USIZE];
81 out[0] = self.len;
82 out[1..].copy_from_slice(&self.bytes);
83 out
84 }
85
86 pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
87 if bytes.len() != Self::STORED_SIZE_USIZE {
88 return Err("corrupted EntityName: invalid size");
89 }
90
91 let len = bytes[0] as usize;
92 if len == 0 || len > MAX_ENTITY_NAME_LEN {
93 return Err("corrupted EntityName: invalid length");
94 }
95 if !bytes[1..=len].is_ascii() {
96 return Err("corrupted EntityName: invalid encoding");
97 }
98 if bytes[1 + len..].iter().any(|&b| b != 0) {
99 return Err("corrupted EntityName: non-zero padding");
100 }
101
102 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
103 name.copy_from_slice(&bytes[1..]);
104
105 Ok(Self {
106 len: len as u8,
107 bytes: name,
108 })
109 }
110
111 pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
112 Self::from_bytes(bytes)
113 }
114
115 #[must_use]
116 pub const fn max_storable() -> Self {
117 Self {
118 len: MAX_ENTITY_NAME_LEN as u8,
119 bytes: [b'z'; MAX_ENTITY_NAME_LEN],
120 }
121 }
122}
123
124impl TryFrom<&[u8]> for EntityName {
125 type Error = &'static str;
126
127 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
128 Self::try_from_bytes(bytes)
129 }
130}
131
132impl Ord for EntityName {
133 fn cmp(&self, other: &Self) -> Ordering {
134 self.len
135 .cmp(&other.len)
136 .then_with(|| self.bytes.cmp(&other.bytes))
137 }
138}
139
140impl PartialOrd for EntityName {
141 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
142 Some(self.cmp(other))
143 }
144}
145
146impl Display for EntityName {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 f.write_str(self.as_str())
149 }
150}
151
152impl fmt::Debug for EntityName {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 write!(f, "EntityName({})", self.as_str())
155 }
156}
157
158#[derive(Clone, Copy, Eq, Hash, PartialEq)]
163pub struct IndexName {
164 pub len: u16,
165 pub bytes: [u8; MAX_INDEX_NAME_LEN],
166}
167
168impl IndexName {
169 pub const STORED_SIZE: u32 = 2 + MAX_INDEX_NAME_LEN as u32;
170 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
171
172 #[must_use]
173 pub fn from_parts(entity: &EntityName, fields: &[&str]) -> Self {
174 assert!(
175 fields.len() <= MAX_INDEX_FIELDS,
176 "index has too many fields"
177 );
178
179 let mut out = [0u8; MAX_INDEX_NAME_LEN];
180 let mut len = 0usize;
181
182 Self::push_ascii(&mut out, &mut len, entity.as_bytes());
183
184 for field in fields {
185 assert!(
186 field.len() <= MAX_INDEX_FIELD_NAME_LEN,
187 "index field name too long"
188 );
189 Self::push_ascii(&mut out, &mut len, b"|");
190 Self::push_ascii(&mut out, &mut len, field.as_bytes());
191 }
192
193 Self {
194 len: len as u16,
195 bytes: out,
196 }
197 }
198
199 #[must_use]
200 pub fn as_bytes(&self) -> &[u8] {
201 &self.bytes[..self.len as usize]
202 }
203
204 #[must_use]
205 pub fn as_str(&self) -> &str {
206 unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
207 }
208
209 #[must_use]
210 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
211 let mut out = [0u8; Self::STORED_SIZE_USIZE];
212 out[..2].copy_from_slice(&self.len.to_be_bytes());
213 out[2..].copy_from_slice(&self.bytes);
214 out
215 }
216
217 pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
218 if bytes.len() != Self::STORED_SIZE_USIZE {
219 return Err("corrupted IndexName: invalid size");
220 }
221
222 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
223 if len == 0 || len > MAX_INDEX_NAME_LEN {
224 return Err("corrupted IndexName: invalid length");
225 }
226 if !bytes[2..2 + len].is_ascii() {
227 return Err("corrupted IndexName: invalid encoding");
228 }
229 if bytes[2 + len..].iter().any(|&b| b != 0) {
230 return Err("corrupted IndexName: non-zero padding");
231 }
232
233 let mut name = [0u8; MAX_INDEX_NAME_LEN];
234 name.copy_from_slice(&bytes[2..]);
235
236 Ok(Self {
237 len: len as u16,
238 bytes: name,
239 })
240 }
241
242 pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
243 Self::from_bytes(bytes)
244 }
245
246 fn push_ascii(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
247 assert!(bytes.is_ascii(), "index name must be ASCII");
248 assert!(
249 *len + bytes.len() <= MAX_INDEX_NAME_LEN,
250 "index name too long"
251 );
252
253 out[*len..*len + bytes.len()].copy_from_slice(bytes);
254 *len += bytes.len();
255 }
256
257 #[must_use]
258 pub const fn max_storable() -> Self {
259 Self {
260 len: MAX_INDEX_NAME_LEN as u16,
261 bytes: [b'z'; MAX_INDEX_NAME_LEN],
262 }
263 }
264}
265
266impl TryFrom<&[u8]> for IndexName {
267 type Error = &'static str;
268
269 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
270 Self::try_from_bytes(bytes)
271 }
272}
273
274impl fmt::Debug for IndexName {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 write!(f, "IndexName({})", self.as_str())
277 }
278}
279
280impl Display for IndexName {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 write!(f, "{}", self.as_str())
283 }
284}
285
286impl Ord for IndexName {
287 fn cmp(&self, other: &Self) -> Ordering {
288 self.len
289 .cmp(&other.len)
290 .then_with(|| self.bytes.cmp(&other.bytes))
291 }
292}
293
294impl PartialOrd for IndexName {
295 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
296 Some(self.cmp(other))
297 }
298}
299
300#[cfg(test)]
305mod tests {
306 use super::*;
307
308 const ENTITY_64: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
309 const ENTITY_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
310 const FIELD_64_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
311 const FIELD_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
312 const FIELD_64_C: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
313 const FIELD_64_D: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
314
315 #[test]
316 fn index_name_max_len_matches_limits() {
317 let entity = EntityName::from_static(ENTITY_64);
318 let fields = [FIELD_64_A, FIELD_64_B, FIELD_64_C, FIELD_64_D];
319
320 assert_eq!(entity.as_str().len(), MAX_ENTITY_NAME_LEN);
321 for field in &fields {
322 assert_eq!(field.len(), MAX_INDEX_FIELD_NAME_LEN);
323 }
324 assert_eq!(fields.len(), MAX_INDEX_FIELDS);
325
326 let name = IndexName::from_parts(&entity, &fields);
327
328 assert_eq!(name.as_bytes().len(), MAX_INDEX_NAME_LEN);
329 }
330
331 #[test]
332 fn index_name_max_size_roundtrip_and_ordering() {
333 let entity_a = EntityName::from_static(ENTITY_64);
334 let entity_b = EntityName::from_static(ENTITY_64_B);
335 let fields_a = [FIELD_64_A, FIELD_64_A, FIELD_64_A, FIELD_64_A];
336 let fields_b = [FIELD_64_B, FIELD_64_B, FIELD_64_B, FIELD_64_B];
337
338 let idx_a = IndexName::from_parts(&entity_a, &fields_a);
339 let idx_b = IndexName::from_parts(&entity_b, &fields_b);
340
341 assert_eq!(idx_a.as_bytes().len(), MAX_INDEX_NAME_LEN);
342 assert_eq!(idx_b.as_bytes().len(), MAX_INDEX_NAME_LEN);
343
344 let decoded = IndexName::from_bytes(&idx_a.to_bytes()).unwrap();
345 assert_eq!(idx_a, decoded);
346
347 assert_eq!(idx_a.cmp(&idx_b), idx_a.to_bytes().cmp(&idx_b.to_bytes()));
348 }
349
350 #[test]
351 #[should_panic(expected = "index has too many fields")]
352 fn rejects_too_many_index_fields() {
353 let entity = EntityName::from_static("entity");
354 let fields = ["a", "b", "c", "d", "e"];
355 let _ = IndexName::from_parts(&entity, &fields);
356 }
357
358 #[test]
359 #[should_panic(expected = "index field name too long")]
360 fn rejects_index_field_over_len() {
361 let entity = EntityName::from_static("entity");
362 let long_field = "a".repeat(MAX_INDEX_FIELD_NAME_LEN + 1);
363 let fields = [long_field.as_str()];
364 let _ = IndexName::from_parts(&entity, &fields);
365 }
366
367 #[test]
368 fn entity_from_static_roundtrip() {
369 let e = EntityName::from_static("user");
370 assert_eq!(e.len(), 4);
371 assert_eq!(e.as_str(), "user");
372 }
373
374 #[test]
375 #[should_panic(expected = "entity name length out of bounds")]
376 fn entity_rejects_empty() {
377 let _ = EntityName::from_static("");
378 }
379
380 #[test]
381 #[should_panic(expected = "entity name must be ASCII")]
382 fn entity_rejects_non_ascii() {
383 let _ = EntityName::from_static("usér");
384 }
385
386 #[test]
387 fn entity_storage_roundtrip() {
388 let e = EntityName::from_static("entity_name");
389 let bytes = e.to_bytes();
390 let decoded = EntityName::from_bytes(&bytes).unwrap();
391 assert_eq!(e, decoded);
392 }
393
394 #[test]
395 fn entity_rejects_invalid_size() {
396 let buf = vec![0u8; EntityName::STORED_SIZE_USIZE - 1];
397 assert!(EntityName::from_bytes(&buf).is_err());
398 }
399
400 #[test]
401 fn entity_rejects_invalid_size_oversized() {
402 let buf = vec![0u8; EntityName::STORED_SIZE_USIZE + 1];
403 assert!(EntityName::from_bytes(&buf).is_err());
404 }
405
406 #[test]
407 fn entity_rejects_len_over_max() {
408 let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
409 buf[0] = (MAX_ENTITY_NAME_LEN as u8).saturating_add(1);
410 assert!(EntityName::from_bytes(&buf).is_err());
411 }
412
413 #[test]
414 fn entity_rejects_non_ascii_from_bytes() {
415 let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
416 buf[0] = 1;
417 buf[1] = 0xFF;
418 assert!(EntityName::from_bytes(&buf).is_err());
419 }
420
421 #[test]
422 fn entity_rejects_non_zero_padding() {
423 let e = EntityName::from_static("user");
424 let mut bytes = e.to_bytes();
425 bytes[1 + e.len()] = b'x';
426 assert!(EntityName::from_bytes(&bytes).is_err());
427 }
428
429 #[test]
430 fn entity_ordering_matches_bytes() {
431 let a = EntityName::from_static("abc");
432 let b = EntityName::from_static("abd");
433 let c = EntityName::from_static("abcx");
434
435 assert_eq!(a.cmp(&b), a.to_bytes().cmp(&b.to_bytes()));
436 assert_eq!(a.cmp(&c), a.to_bytes().cmp(&c.to_bytes()));
437 }
438
439 #[test]
440 fn entity_ordering_b_vs_aa() {
441 let b = EntityName::from_static("b");
442 let aa = EntityName::from_static("aa");
443 assert_eq!(b.cmp(&aa), b.to_bytes().cmp(&aa.to_bytes()));
444 }
445
446 #[test]
447 fn entity_ordering_prefix_matches_bytes() {
448 let a = EntityName::from_static("a");
449 let aa = EntityName::from_static("aa");
450 assert_eq!(a.cmp(&aa), a.to_bytes().cmp(&aa.to_bytes()));
451 }
452
453 #[test]
454 fn index_single_field_format() {
455 let entity = EntityName::from_static("user");
456 let idx = IndexName::from_parts(&entity, &["email"]);
457
458 assert_eq!(idx.as_str(), "user|email");
459 }
460
461 #[test]
462 fn index_field_order_is_preserved() {
463 let entity = EntityName::from_static("user");
464 let idx = IndexName::from_parts(&entity, &["a", "b", "c"]);
465
466 assert_eq!(idx.as_str(), "user|a|b|c");
467 }
468
469 #[test]
470 fn index_storage_roundtrip() {
471 let entity = EntityName::from_static("user");
472 let idx = IndexName::from_parts(&entity, &["a", "b"]);
473
474 let bytes = idx.to_bytes();
475 let decoded = IndexName::from_bytes(&bytes).unwrap();
476
477 assert_eq!(idx, decoded);
478 }
479
480 #[test]
481 fn index_rejects_zero_len() {
482 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
483 buf[0] = 0;
484 assert!(IndexName::from_bytes(&buf).is_err());
485 }
486
487 #[test]
488 fn index_rejects_invalid_size_oversized() {
489 let buf = vec![0u8; IndexName::STORED_SIZE_USIZE + 1];
490 assert!(IndexName::from_bytes(&buf).is_err());
491 }
492
493 #[test]
494 fn index_rejects_len_over_max() {
495 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
496 let len = (MAX_INDEX_NAME_LEN as u16).saturating_add(1);
497 buf[..2].copy_from_slice(&len.to_be_bytes());
498 assert!(IndexName::from_bytes(&buf).is_err());
499 }
500
501 #[test]
502 fn index_rejects_non_ascii_from_bytes() {
503 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
504 buf[..2].copy_from_slice(&1u16.to_be_bytes());
505 buf[2] = 0xFF;
506 assert!(IndexName::from_bytes(&buf).is_err());
507 }
508
509 #[test]
510 fn index_rejects_non_zero_padding() {
511 let entity = EntityName::from_static("user");
512 let idx = IndexName::from_parts(&entity, &["a"]);
513 let mut bytes = idx.to_bytes();
514 bytes[2 + idx.len as usize] = b'x';
515 assert!(IndexName::from_bytes(&bytes).is_err());
516 }
517
518 #[test]
519 fn index_ordering_matches_bytes() {
520 let entity = EntityName::from_static("user");
521
522 let a = IndexName::from_parts(&entity, &["a"]);
523 let ab = IndexName::from_parts(&entity, &["a", "b"]);
524 let b = IndexName::from_parts(&entity, &["b"]);
525
526 assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
527 assert_eq!(ab.cmp(&b), ab.to_bytes().cmp(&b.to_bytes()));
528 }
529
530 #[test]
531 fn index_ordering_prefix_matches_bytes() {
532 let entity = EntityName::from_static("user");
533 let a = IndexName::from_parts(&entity, &["a"]);
534 let ab = IndexName::from_parts(&entity, &["a", "b"]);
535 assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
536 }
537
538 #[test]
539 fn max_storable_orders_last() {
540 let entity = EntityName::from_static("zz");
541 let max = EntityName::max_storable();
542
543 assert!(entity < max);
544 }
545
546 fn gen_ascii(seed: u64, max_len: usize) -> String {
552 let len = (seed as usize % max_len).max(1);
553 let mut out = String::with_capacity(len);
554
555 let mut x = seed;
556 for _ in 0..len {
557 x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
559 let c = b'a' + (x % 26) as u8;
560 out.push(c as char);
561 }
562
563 out
564 }
565
566 #[test]
567 fn fuzz_entity_name_roundtrip_and_ordering() {
568 const RUNS: u64 = 1_000;
569
570 let mut prev: Option<EntityName> = None;
571
572 for i in 1..=RUNS {
573 let s = gen_ascii(i, MAX_ENTITY_NAME_LEN);
574 let e = EntityName::from_static(Box::leak(s.clone().into_boxed_str()));
575
576 let bytes = e.to_bytes();
578 let decoded = EntityName::from_bytes(&bytes).unwrap();
579 assert_eq!(e, decoded);
580
581 if let Some(p) = prev {
583 let ord_entity = p.cmp(&e);
584 let ord_bytes = p.to_bytes().cmp(&e.to_bytes());
585 assert_eq!(ord_entity, ord_bytes);
586 }
587
588 prev = Some(e);
589 }
590 }
591
592 #[test]
593 fn fuzz_index_name_roundtrip_and_ordering() {
594 const RUNS: u64 = 1_000;
595
596 let entity = EntityName::from_static("entity");
597 let mut prev: Option<IndexName> = None;
598
599 for i in 1..=RUNS {
600 let field_count = (i as usize % MAX_INDEX_FIELDS).max(1);
601
602 let mut field_strings = Vec::with_capacity(field_count);
603 let mut fields = Vec::with_capacity(field_count);
604 let mut string_parts = Vec::with_capacity(field_count + 1);
605
606 string_parts.push(entity.as_str().to_owned());
607
608 for f in 0..field_count {
609 let s = gen_ascii(i * 31 + f as u64, MAX_INDEX_FIELD_NAME_LEN);
610 string_parts.push(s.clone());
611 field_strings.push(s);
612 }
613
614 for s in &field_strings {
615 fields.push(s.as_str());
616 }
617
618 let idx = IndexName::from_parts(&entity, &fields);
619 let expected = string_parts.join("|");
620
621 assert_eq!(idx.as_str(), expected);
623
624 let bytes = idx.to_bytes();
626 let decoded = IndexName::from_bytes(&bytes).unwrap();
627 assert_eq!(idx, decoded);
628
629 if let Some(p_idx) = prev {
631 let ord_idx = p_idx.cmp(&idx);
632 let ord_bytes = p_idx.to_bytes().cmp(&idx.to_bytes());
633 assert_eq!(ord_idx, ord_bytes);
634 }
635
636 prev = Some(idx);
637 }
638 }
639}