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