1#![allow(clippy::cast_possible_truncation)]
2use crate::MAX_INDEX_FIELDS;
9use std::{
10 cmp::Ordering,
11 fmt::{self, Display},
12};
13use thiserror::Error as ThisError;
14
15pub const MAX_ENTITY_NAME_LEN: usize = 64;
20pub const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
21pub const MAX_INDEX_NAME_LEN: usize =
22 MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
23
24#[derive(Debug, ThisError)]
30pub enum EntityNameError {
31 #[error("entity name is empty")]
32 Empty,
33
34 #[error("entity name length {len} exceeds max {max}")]
35 TooLong { len: usize, max: usize },
36
37 #[error("entity name must be ASCII")]
38 NonAscii,
39}
40
41#[derive(Debug, ThisError)]
47pub enum IndexNameError {
48 #[error("index has {len} fields (max {max})")]
49 TooManyFields { len: usize, max: usize },
50
51 #[error("index field name '{field}' exceeds max length {max}")]
52 FieldTooLong { field: String, max: usize },
53
54 #[error("index field name '{field}' must be ASCII")]
55 FieldNonAscii { field: String },
56
57 #[error("index name length {len} exceeds max {max}")]
58 TooLong { len: usize, max: usize },
59}
60
61#[derive(Clone, Copy, Eq, Hash, PartialEq)]
66pub struct EntityName {
67 pub len: u8,
68 pub bytes: [u8; MAX_ENTITY_NAME_LEN],
69}
70
71impl EntityName {
72 pub const STORED_SIZE: u32 = 1 + MAX_ENTITY_NAME_LEN as u32;
73 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
74
75 pub fn try_from_static(name: &'static str) -> Result<Self, EntityNameError> {
77 Self::try_from_str(name)
78 }
79
80 #[must_use]
81 pub(crate) const fn from_static_unchecked(name: &'static str) -> Self {
85 let bytes = name.as_bytes();
86 let len = bytes.len();
87
88 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
89 let mut i = 0;
90 while i < len {
91 let b = bytes[i];
92 out[i] = b;
93 i += 1;
94 }
95
96 Self {
97 len: len as u8,
98 bytes: out,
99 }
100 }
101
102 pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
104 let bytes = name.as_bytes();
105 let len = bytes.len();
106
107 if len == 0 {
108 return Err(EntityNameError::Empty);
109 }
110 if len > MAX_ENTITY_NAME_LEN {
111 return Err(EntityNameError::TooLong {
112 len,
113 max: MAX_ENTITY_NAME_LEN,
114 });
115 }
116 if !bytes.is_ascii() {
117 return Err(EntityNameError::NonAscii);
118 }
119
120 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
121 out[..len].copy_from_slice(bytes);
122
123 Ok(Self {
124 len: len as u8,
125 bytes: out,
126 })
127 }
128
129 #[must_use]
130 pub const fn len(&self) -> usize {
131 self.len as usize
132 }
133
134 #[must_use]
135 pub const fn is_empty(&self) -> bool {
136 self.len == 0
137 }
138
139 #[must_use]
140 pub fn as_bytes(&self) -> &[u8] {
141 &self.bytes[..self.len()]
142 }
143
144 #[must_use]
145 pub fn as_str(&self) -> &str {
146 unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
148 }
149
150 #[must_use]
151 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
152 let mut out = [0u8; Self::STORED_SIZE_USIZE];
153 out[0] = self.len;
154 out[1..].copy_from_slice(&self.bytes);
155 out
156 }
157
158 pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
159 if bytes.len() != Self::STORED_SIZE_USIZE {
160 return Err("corrupted EntityName: invalid size");
161 }
162
163 let len = bytes[0] as usize;
164 if len == 0 || len > MAX_ENTITY_NAME_LEN {
165 return Err("corrupted EntityName: invalid length");
166 }
167 if !bytes[1..=len].is_ascii() {
168 return Err("corrupted EntityName: invalid encoding");
169 }
170 if bytes[1 + len..].iter().any(|&b| b != 0) {
171 return Err("corrupted EntityName: non-zero padding");
172 }
173
174 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
175 name.copy_from_slice(&bytes[1..]);
176
177 Ok(Self {
178 len: len as u8,
179 bytes: name,
180 })
181 }
182
183 pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
184 Self::from_bytes(bytes)
185 }
186
187 #[must_use]
188 pub const fn max_storable() -> Self {
189 Self {
190 len: MAX_ENTITY_NAME_LEN as u8,
191 bytes: [b'z'; MAX_ENTITY_NAME_LEN],
192 }
193 }
194}
195
196impl TryFrom<&[u8]> for EntityName {
197 type Error = &'static str;
198
199 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
200 Self::try_from_bytes(bytes)
201 }
202}
203
204impl Ord for EntityName {
205 fn cmp(&self, other: &Self) -> Ordering {
206 self.len
207 .cmp(&other.len)
208 .then_with(|| self.bytes.cmp(&other.bytes))
209 }
210}
211
212impl PartialOrd for EntityName {
213 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
214 Some(self.cmp(other))
215 }
216}
217
218impl Display for EntityName {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 f.write_str(self.as_str())
221 }
222}
223
224impl fmt::Debug for EntityName {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 write!(f, "EntityName({})", self.as_str())
227 }
228}
229
230#[derive(Clone, Copy, Eq, Hash, PartialEq)]
235pub struct IndexName {
236 pub len: u16,
237 pub bytes: [u8; MAX_INDEX_NAME_LEN],
238}
239
240impl IndexName {
241 pub const STORED_SIZE: u32 = 2 + MAX_INDEX_NAME_LEN as u32;
242 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE as usize;
243
244 #[must_use]
245 pub(crate) fn from_parts_unchecked(entity: &EntityName, fields: &[&str]) -> Self {
249 debug_assert!(
250 fields.len() <= MAX_INDEX_FIELDS,
251 "index has too many fields"
252 );
253
254 let mut out = [0u8; MAX_INDEX_NAME_LEN];
255 let mut len = 0usize;
256
257 Self::push_ascii(&mut out, &mut len, entity.as_bytes());
258
259 for field in fields {
260 debug_assert!(
261 field.len() <= MAX_INDEX_FIELD_NAME_LEN,
262 "index field name too long"
263 );
264 Self::push_ascii(&mut out, &mut len, b"|");
265 Self::push_ascii(&mut out, &mut len, field.as_bytes());
266 }
267
268 Self {
269 len: len as u16,
270 bytes: out,
271 }
272 }
273
274 pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
276 if fields.len() > MAX_INDEX_FIELDS {
278 return Err(IndexNameError::TooManyFields {
279 len: fields.len(),
280 max: MAX_INDEX_FIELDS,
281 });
282 }
283
284 let mut total_len = entity.len();
285 for field in fields {
286 let field_len = field.len();
287 if field_len > MAX_INDEX_FIELD_NAME_LEN {
288 return Err(IndexNameError::FieldTooLong {
289 field: (*field).to_string(),
290 max: MAX_INDEX_FIELD_NAME_LEN,
291 });
292 }
293 if !field.is_ascii() {
294 return Err(IndexNameError::FieldNonAscii {
295 field: (*field).to_string(),
296 });
297 }
298 total_len = total_len.saturating_add(1 + field_len);
299 }
300
301 if total_len > MAX_INDEX_NAME_LEN {
302 return Err(IndexNameError::TooLong {
303 len: total_len,
304 max: MAX_INDEX_NAME_LEN,
305 });
306 }
307
308 let mut out = [0u8; MAX_INDEX_NAME_LEN];
310 let mut len = 0usize;
311
312 Self::push_bytes(&mut out, &mut len, entity.as_bytes());
313 for field in fields {
314 Self::push_bytes(&mut out, &mut len, b"|");
315 Self::push_bytes(&mut out, &mut len, field.as_bytes());
316 }
317
318 Ok(Self {
319 len: len as u16,
320 bytes: out,
321 })
322 }
323
324 #[must_use]
325 pub fn as_bytes(&self) -> &[u8] {
326 &self.bytes[..self.len as usize]
327 }
328
329 #[must_use]
330 pub fn as_str(&self) -> &str {
331 unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
332 }
333
334 #[must_use]
335 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
336 let mut out = [0u8; Self::STORED_SIZE_USIZE];
337 out[..2].copy_from_slice(&self.len.to_be_bytes());
338 out[2..].copy_from_slice(&self.bytes);
339 out
340 }
341
342 pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
343 if bytes.len() != Self::STORED_SIZE_USIZE {
344 return Err("corrupted IndexName: invalid size");
345 }
346
347 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
348 if len == 0 || len > MAX_INDEX_NAME_LEN {
349 return Err("corrupted IndexName: invalid length");
350 }
351 if !bytes[2..2 + len].is_ascii() {
352 return Err("corrupted IndexName: invalid encoding");
353 }
354 if bytes[2 + len..].iter().any(|&b| b != 0) {
355 return Err("corrupted IndexName: non-zero padding");
356 }
357
358 let mut name = [0u8; MAX_INDEX_NAME_LEN];
359 name.copy_from_slice(&bytes[2..]);
360
361 Ok(Self {
362 len: len as u16,
363 bytes: name,
364 })
365 }
366
367 pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
368 Self::from_bytes(bytes)
369 }
370
371 fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
372 let end = *len + bytes.len();
373 out[*len..end].copy_from_slice(bytes);
374 *len = end;
375 }
376
377 fn push_ascii(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
378 debug_assert!(bytes.is_ascii(), "index name must be ASCII");
379 debug_assert!(
380 *len + bytes.len() <= MAX_INDEX_NAME_LEN,
381 "index name too long"
382 );
383
384 out[*len..*len + bytes.len()].copy_from_slice(bytes);
385 *len += bytes.len();
386 }
387
388 #[must_use]
389 pub const fn max_storable() -> Self {
390 Self {
391 len: MAX_INDEX_NAME_LEN as u16,
392 bytes: [b'z'; MAX_INDEX_NAME_LEN],
393 }
394 }
395}
396
397impl TryFrom<&[u8]> for IndexName {
398 type Error = &'static str;
399
400 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
401 Self::try_from_bytes(bytes)
402 }
403}
404
405impl fmt::Debug for IndexName {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 write!(f, "IndexName({})", self.as_str())
408 }
409}
410
411impl Display for IndexName {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 write!(f, "{}", self.as_str())
414 }
415}
416
417impl Ord for IndexName {
418 fn cmp(&self, other: &Self) -> Ordering {
419 self.len
420 .cmp(&other.len)
421 .then_with(|| self.bytes.cmp(&other.bytes))
422 }
423}
424
425impl PartialOrd for IndexName {
426 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
427 Some(self.cmp(other))
428 }
429}
430
431#[cfg(test)]
436mod tests {
437 use super::*;
438
439 const ENTITY_64: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
440 const ENTITY_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
441 const FIELD_64_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
442 const FIELD_64_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
443 const FIELD_64_C: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
444 const FIELD_64_D: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
445
446 #[test]
447 fn index_name_max_len_matches_limits() {
448 let entity = EntityName::from_static_unchecked(ENTITY_64);
449 let fields = [FIELD_64_A, FIELD_64_B, FIELD_64_C, FIELD_64_D];
450
451 assert_eq!(entity.as_str().len(), MAX_ENTITY_NAME_LEN);
452 for field in &fields {
453 assert_eq!(field.len(), MAX_INDEX_FIELD_NAME_LEN);
454 }
455 assert_eq!(fields.len(), MAX_INDEX_FIELDS);
456
457 let name = IndexName::from_parts_unchecked(&entity, &fields);
458
459 assert_eq!(name.as_bytes().len(), MAX_INDEX_NAME_LEN);
460 }
461
462 #[test]
463 fn index_name_max_size_roundtrip_and_ordering() {
464 let entity_a = EntityName::from_static_unchecked(ENTITY_64);
465 let entity_b = EntityName::from_static_unchecked(ENTITY_64_B);
466 let fields_a = [FIELD_64_A, FIELD_64_A, FIELD_64_A, FIELD_64_A];
467 let fields_b = [FIELD_64_B, FIELD_64_B, FIELD_64_B, FIELD_64_B];
468
469 let idx_a = IndexName::from_parts_unchecked(&entity_a, &fields_a);
470 let idx_b = IndexName::from_parts_unchecked(&entity_b, &fields_b);
471
472 assert_eq!(idx_a.as_bytes().len(), MAX_INDEX_NAME_LEN);
473 assert_eq!(idx_b.as_bytes().len(), MAX_INDEX_NAME_LEN);
474
475 let decoded = IndexName::from_bytes(&idx_a.to_bytes()).unwrap();
476 assert_eq!(idx_a, decoded);
477
478 assert_eq!(idx_a.cmp(&idx_b), idx_a.to_bytes().cmp(&idx_b.to_bytes()));
479 }
480
481 #[test]
482 fn rejects_too_many_index_fields() {
483 let entity = EntityName::try_from_str("entity").expect("entity name");
484 let fields = ["a", "b", "c", "d", "e"];
485 let err = IndexName::try_from_parts(&entity, &fields).unwrap_err();
486 assert!(matches!(err, IndexNameError::TooManyFields { .. }));
487 }
488
489 #[test]
490 fn rejects_index_field_over_len() {
491 let entity = EntityName::try_from_str("entity").expect("entity name");
492 let long_field = "a".repeat(MAX_INDEX_FIELD_NAME_LEN + 1);
493 let fields = [long_field.as_str()];
494 let err = IndexName::try_from_parts(&entity, &fields).unwrap_err();
495 assert!(matches!(err, IndexNameError::FieldTooLong { .. }));
496 }
497
498 #[test]
499 fn entity_from_static_roundtrip() {
500 let e = EntityName::try_from_static("user").expect("entity name");
501 assert_eq!(e.len(), 4);
502 assert_eq!(e.as_str(), "user");
503 }
504
505 #[test]
506 fn entity_try_from_static_rejects_empty() {
507 let err = EntityName::try_from_static("").unwrap_err();
508 assert!(matches!(err, EntityNameError::Empty));
509 }
510
511 #[test]
512 fn entity_try_from_static_rejects_len_over_max() {
513 let s = "a".repeat(MAX_ENTITY_NAME_LEN + 1);
514 let leaked = Box::leak(s.into_boxed_str());
515 let err = EntityName::try_from_static(leaked).unwrap_err();
516 assert!(matches!(err, EntityNameError::TooLong { .. }));
517 }
518
519 #[test]
520 fn entity_rejects_empty() {
521 let err = EntityName::try_from_str("").unwrap_err();
522 assert!(matches!(err, EntityNameError::Empty));
523 }
524
525 #[test]
526 fn entity_rejects_non_ascii() {
527 let err = EntityName::try_from_str("usér").unwrap_err();
528 assert!(matches!(err, EntityNameError::NonAscii));
529 }
530
531 #[test]
532 fn entity_storage_roundtrip() {
533 let e = EntityName::try_from_static("entity_name").expect("entity name");
534 let bytes = e.to_bytes();
535 let decoded = EntityName::from_bytes(&bytes).unwrap();
536 assert_eq!(e, decoded);
537 }
538
539 #[test]
540 fn entity_rejects_invalid_size() {
541 let buf = vec![0u8; EntityName::STORED_SIZE_USIZE - 1];
542 assert!(EntityName::from_bytes(&buf).is_err());
543 }
544
545 #[test]
546 fn entity_rejects_invalid_size_oversized() {
547 let buf = vec![0u8; EntityName::STORED_SIZE_USIZE + 1];
548 assert!(EntityName::from_bytes(&buf).is_err());
549 }
550
551 #[test]
552 fn entity_rejects_len_over_max() {
553 let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
554 buf[0] = (MAX_ENTITY_NAME_LEN as u8).saturating_add(1);
555 assert!(EntityName::from_bytes(&buf).is_err());
556 }
557
558 #[test]
559 fn entity_rejects_non_ascii_from_bytes() {
560 let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
561 buf[0] = 1;
562 buf[1] = 0xFF;
563 assert!(EntityName::from_bytes(&buf).is_err());
564 }
565
566 #[test]
567 fn entity_rejects_non_zero_padding() {
568 let e = EntityName::from_static_unchecked("user");
569 let mut bytes = e.to_bytes();
570 bytes[1 + e.len()] = b'x';
571 assert!(EntityName::from_bytes(&bytes).is_err());
572 }
573
574 #[test]
575 fn entity_ordering_matches_bytes() {
576 let a = EntityName::from_static_unchecked("abc");
577 let b = EntityName::from_static_unchecked("abd");
578 let c = EntityName::from_static_unchecked("abcx");
579
580 assert_eq!(a.cmp(&b), a.to_bytes().cmp(&b.to_bytes()));
581 assert_eq!(a.cmp(&c), a.to_bytes().cmp(&c.to_bytes()));
582 }
583
584 #[test]
585 fn entity_ordering_b_vs_aa() {
586 let b = EntityName::from_static_unchecked("b");
587 let aa = EntityName::from_static_unchecked("aa");
588 assert_eq!(b.cmp(&aa), b.to_bytes().cmp(&aa.to_bytes()));
589 }
590
591 #[test]
592 fn entity_ordering_prefix_matches_bytes() {
593 let a = EntityName::from_static_unchecked("a");
594 let aa = EntityName::from_static_unchecked("aa");
595 assert_eq!(a.cmp(&aa), a.to_bytes().cmp(&aa.to_bytes()));
596 }
597
598 #[test]
599 fn index_single_field_format() {
600 let entity = EntityName::from_static_unchecked("user");
601 let idx = IndexName::from_parts_unchecked(&entity, &["email"]);
602
603 assert_eq!(idx.as_str(), "user|email");
604 }
605
606 #[test]
607 fn index_field_order_is_preserved() {
608 let entity = EntityName::from_static_unchecked("user");
609 let idx = IndexName::from_parts_unchecked(&entity, &["a", "b", "c"]);
610
611 assert_eq!(idx.as_str(), "user|a|b|c");
612 }
613
614 #[test]
615 fn index_storage_roundtrip() {
616 let entity = EntityName::from_static_unchecked("user");
617 let idx = IndexName::from_parts_unchecked(&entity, &["a", "b"]);
618
619 let bytes = idx.to_bytes();
620 let decoded = IndexName::from_bytes(&bytes).unwrap();
621
622 assert_eq!(idx, decoded);
623 }
624
625 #[test]
626 fn index_rejects_zero_len() {
627 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
628 buf[0] = 0;
629 assert!(IndexName::from_bytes(&buf).is_err());
630 }
631
632 #[test]
633 fn index_rejects_invalid_size_oversized() {
634 let buf = vec![0u8; IndexName::STORED_SIZE_USIZE + 1];
635 assert!(IndexName::from_bytes(&buf).is_err());
636 }
637
638 #[test]
639 fn index_rejects_len_over_max() {
640 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
641 let len = (MAX_INDEX_NAME_LEN as u16).saturating_add(1);
642 buf[..2].copy_from_slice(&len.to_be_bytes());
643 assert!(IndexName::from_bytes(&buf).is_err());
644 }
645
646 #[test]
647 fn index_rejects_non_ascii_from_bytes() {
648 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
649 buf[..2].copy_from_slice(&1u16.to_be_bytes());
650 buf[2] = 0xFF;
651 assert!(IndexName::from_bytes(&buf).is_err());
652 }
653
654 #[test]
655 fn index_rejects_non_zero_padding() {
656 let entity = EntityName::from_static_unchecked("user");
657 let idx = IndexName::from_parts_unchecked(&entity, &["a"]);
658 let mut bytes = idx.to_bytes();
659 bytes[2 + idx.len as usize] = b'x';
660 assert!(IndexName::from_bytes(&bytes).is_err());
661 }
662
663 #[test]
664 fn index_ordering_matches_bytes() {
665 let entity = EntityName::from_static_unchecked("user");
666
667 let a = IndexName::from_parts_unchecked(&entity, &["a"]);
668 let ab = IndexName::from_parts_unchecked(&entity, &["a", "b"]);
669 let b = IndexName::from_parts_unchecked(&entity, &["b"]);
670
671 assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
672 assert_eq!(ab.cmp(&b), ab.to_bytes().cmp(&b.to_bytes()));
673 }
674
675 #[test]
676 fn index_ordering_prefix_matches_bytes() {
677 let entity = EntityName::from_static_unchecked("user");
678 let a = IndexName::from_parts_unchecked(&entity, &["a"]);
679 let ab = IndexName::from_parts_unchecked(&entity, &["a", "b"]);
680 assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
681 }
682
683 #[test]
684 fn max_storable_orders_last() {
685 let entity = EntityName::from_static_unchecked("zz");
686 let max = EntityName::max_storable();
687
688 assert!(entity < max);
689 }
690
691 fn gen_ascii(seed: u64, max_len: usize) -> String {
697 let len = (seed as usize % max_len).max(1);
698 let mut out = String::with_capacity(len);
699
700 let mut x = seed;
701 for _ in 0..len {
702 x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
704 let c = b'a' + (x % 26) as u8;
705 out.push(c as char);
706 }
707
708 out
709 }
710
711 #[test]
712 fn fuzz_entity_name_roundtrip_and_ordering() {
713 const RUNS: u64 = 1_000;
714
715 let mut prev: Option<EntityName> = None;
716
717 for i in 1..=RUNS {
718 let s = gen_ascii(i, MAX_ENTITY_NAME_LEN);
719 let e = EntityName::from_static_unchecked(Box::leak(s.clone().into_boxed_str()));
720
721 let bytes = e.to_bytes();
723 let decoded = EntityName::from_bytes(&bytes).unwrap();
724 assert_eq!(e, decoded);
725
726 if let Some(p) = prev {
728 let ord_entity = p.cmp(&e);
729 let ord_bytes = p.to_bytes().cmp(&e.to_bytes());
730 assert_eq!(ord_entity, ord_bytes);
731 }
732
733 prev = Some(e);
734 }
735 }
736
737 #[test]
738 fn fuzz_index_name_roundtrip_and_ordering() {
739 const RUNS: u64 = 1_000;
740
741 let entity = EntityName::from_static_unchecked("entity");
742 let mut prev: Option<IndexName> = None;
743
744 for i in 1..=RUNS {
745 let field_count = (i as usize % MAX_INDEX_FIELDS).max(1);
746
747 let mut field_strings = Vec::with_capacity(field_count);
748 let mut fields = Vec::with_capacity(field_count);
749 let mut string_parts = Vec::with_capacity(field_count + 1);
750
751 string_parts.push(entity.as_str().to_owned());
752
753 for f in 0..field_count {
754 let s = gen_ascii(i * 31 + f as u64, MAX_INDEX_FIELD_NAME_LEN);
755 string_parts.push(s.clone());
756 field_strings.push(s);
757 }
758
759 for s in &field_strings {
760 fields.push(s.as_str());
761 }
762
763 let idx = IndexName::from_parts_unchecked(&entity, &fields);
764 let expected = string_parts.join("|");
765
766 assert_eq!(idx.as_str(), expected);
768
769 let bytes = idx.to_bytes();
771 let decoded = IndexName::from_bytes(&bytes).unwrap();
772 assert_eq!(idx, decoded);
773
774 if let Some(p_idx) = prev {
776 let ord_idx = p_idx.cmp(&idx);
777 let ord_bytes = p_idx.to_bytes().cmp(&idx.to_bytes());
778 assert_eq!(ord_idx, ord_bytes);
779 }
780
781 prev = Some(idx);
782 }
783 }
784}