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 = 48;
14pub const MAX_INDEX_FIELD_NAME_LEN: usize = 48;
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: u8,
153 pub bytes: [u8; MAX_INDEX_NAME_LEN],
154}
155
156impl IndexName {
157 pub const STORED_SIZE: u32 = 1 + 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 u8,
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[0] = self.len;
201 out[1..].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 = bytes[0] as usize;
211 if len == 0 || len > MAX_INDEX_NAME_LEN {
212 return Err("invalid IndexName length");
213 }
214 if !bytes[1..=len].is_ascii() {
215 return Err("invalid IndexName encoding");
216 }
217 if bytes[1 + 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[1..]);
223
224 Ok(Self {
225 len: len as u8,
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 u8,
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_48: &str = "0123456789abcdef0123456789abcdef0123456789abcdef";
285 const FIELD_48_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
286 const FIELD_48_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
287 const FIELD_48_C: &str = "cccccccccccccccccccccccccccccccccccccccccccccccc";
288 const FIELD_48_D: &str = "dddddddddddddddddddddddddddddddddddddddddddddddd";
289
290 #[test]
291 fn index_name_max_len_matches_limits() {
292 let entity = EntityName::from_static(ENTITY_48);
293 let fields = [FIELD_48_A, FIELD_48_B, FIELD_48_C, FIELD_48_D];
294
295 assert_eq!(entity.as_str().len(), MAX_ENTITY_NAME_LEN);
296 for field in &fields {
297 assert_eq!(field.len(), MAX_INDEX_FIELD_NAME_LEN);
298 }
299 assert_eq!(fields.len(), MAX_INDEX_FIELDS);
300
301 let name = IndexName::from_parts(&entity, &fields);
302
303 assert_eq!(name.as_bytes().len(), MAX_INDEX_NAME_LEN);
304 }
305
306 #[test]
307 #[should_panic(expected = "index has too many fields")]
308 fn rejects_too_many_index_fields() {
309 let entity = EntityName::from_static("entity");
310 let fields = ["a", "b", "c", "d", "e"];
311 let _ = IndexName::from_parts(&entity, &fields);
312 }
313
314 #[test]
315 #[should_panic(expected = "index field name too long")]
316 fn rejects_index_field_over_len() {
317 let entity = EntityName::from_static("entity");
318 let long_field = "a".repeat(MAX_INDEX_FIELD_NAME_LEN + 1);
319 let fields = [long_field.as_str()];
320 let _ = IndexName::from_parts(&entity, &fields);
321 }
322
323 #[test]
324 fn entity_from_static_roundtrip() {
325 let e = EntityName::from_static("user");
326 assert_eq!(e.len(), 4);
327 assert_eq!(e.as_str(), "user");
328 }
329
330 #[test]
331 #[should_panic(expected = "entity name length out of bounds")]
332 fn entity_rejects_empty() {
333 let _ = EntityName::from_static("");
334 }
335
336 #[test]
337 #[should_panic(expected = "entity name must be ASCII")]
338 fn entity_rejects_non_ascii() {
339 let _ = EntityName::from_static("usér");
340 }
341
342 #[test]
343 fn entity_storage_roundtrip() {
344 let e = EntityName::from_static("entity_name");
345 let bytes = e.to_bytes();
346 let decoded = EntityName::from_bytes(&bytes).unwrap();
347 assert_eq!(e, decoded);
348 }
349
350 #[test]
351 fn entity_rejects_invalid_size() {
352 let buf = vec![0u8; EntityName::STORED_SIZE_USIZE - 1];
353 assert!(EntityName::from_bytes(&buf).is_err());
354 }
355
356 #[test]
357 fn entity_rejects_invalid_size_oversized() {
358 let buf = vec![0u8; EntityName::STORED_SIZE_USIZE + 1];
359 assert!(EntityName::from_bytes(&buf).is_err());
360 }
361
362 #[test]
363 fn entity_rejects_len_over_max() {
364 let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
365 buf[0] = (MAX_ENTITY_NAME_LEN as u8).saturating_add(1);
366 assert!(EntityName::from_bytes(&buf).is_err());
367 }
368
369 #[test]
370 fn entity_rejects_non_ascii_from_bytes() {
371 let mut buf = [0u8; EntityName::STORED_SIZE_USIZE];
372 buf[0] = 1;
373 buf[1] = 0xFF;
374 assert!(EntityName::from_bytes(&buf).is_err());
375 }
376
377 #[test]
378 fn entity_rejects_non_zero_padding() {
379 let e = EntityName::from_static("user");
380 let mut bytes = e.to_bytes();
381 bytes[1 + e.len()] = b'x';
382 assert!(EntityName::from_bytes(&bytes).is_err());
383 }
384
385 #[test]
386 fn entity_ordering_matches_bytes() {
387 let a = EntityName::from_static("abc");
388 let b = EntityName::from_static("abd");
389 let c = EntityName::from_static("abcx");
390
391 assert_eq!(a.cmp(&b), a.to_bytes().cmp(&b.to_bytes()));
392 assert_eq!(a.cmp(&c), a.to_bytes().cmp(&c.to_bytes()));
393 }
394
395 #[test]
396 fn entity_ordering_b_vs_aa() {
397 let b = EntityName::from_static("b");
398 let aa = EntityName::from_static("aa");
399 assert_eq!(b.cmp(&aa), b.to_bytes().cmp(&aa.to_bytes()));
400 }
401
402 #[test]
403 fn entity_ordering_prefix_matches_bytes() {
404 let a = EntityName::from_static("a");
405 let aa = EntityName::from_static("aa");
406 assert_eq!(a.cmp(&aa), a.to_bytes().cmp(&aa.to_bytes()));
407 }
408
409 #[test]
410 fn index_single_field_format() {
411 let entity = EntityName::from_static("user");
412 let idx = IndexName::from_parts(&entity, &["email"]);
413
414 assert_eq!(idx.as_str(), "user|email");
415 }
416
417 #[test]
418 fn index_field_order_is_preserved() {
419 let entity = EntityName::from_static("user");
420 let idx = IndexName::from_parts(&entity, &["a", "b", "c"]);
421
422 assert_eq!(idx.as_str(), "user|a|b|c");
423 }
424
425 #[test]
426 fn index_storage_roundtrip() {
427 let entity = EntityName::from_static("user");
428 let idx = IndexName::from_parts(&entity, &["a", "b"]);
429
430 let bytes = idx.to_bytes();
431 let decoded = IndexName::from_bytes(&bytes).unwrap();
432
433 assert_eq!(idx, decoded);
434 }
435
436 #[test]
437 fn index_rejects_zero_len() {
438 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
439 buf[0] = 0;
440 assert!(IndexName::from_bytes(&buf).is_err());
441 }
442
443 #[test]
444 fn index_rejects_invalid_size_oversized() {
445 let buf = vec![0u8; IndexName::STORED_SIZE_USIZE + 1];
446 assert!(IndexName::from_bytes(&buf).is_err());
447 }
448
449 #[test]
450 fn index_rejects_len_over_max() {
451 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
452 buf[0] = (MAX_INDEX_NAME_LEN as u8).saturating_add(1);
453 assert!(IndexName::from_bytes(&buf).is_err());
454 }
455
456 #[test]
457 fn index_rejects_non_ascii_from_bytes() {
458 let mut buf = [0u8; IndexName::STORED_SIZE_USIZE];
459 buf[0] = 1;
460 buf[1] = 0xFF;
461 assert!(IndexName::from_bytes(&buf).is_err());
462 }
463
464 #[test]
465 fn index_rejects_non_zero_padding() {
466 let entity = EntityName::from_static("user");
467 let idx = IndexName::from_parts(&entity, &["a"]);
468 let mut bytes = idx.to_bytes();
469 bytes[1 + idx.len as usize] = b'x';
470 assert!(IndexName::from_bytes(&bytes).is_err());
471 }
472
473 #[test]
474 fn index_ordering_matches_bytes() {
475 let entity = EntityName::from_static("user");
476
477 let a = IndexName::from_parts(&entity, &["a"]);
478 let ab = IndexName::from_parts(&entity, &["a", "b"]);
479 let b = IndexName::from_parts(&entity, &["b"]);
480
481 assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
482 assert_eq!(ab.cmp(&b), ab.to_bytes().cmp(&b.to_bytes()));
483 }
484
485 #[test]
486 fn index_ordering_prefix_matches_bytes() {
487 let entity = EntityName::from_static("user");
488 let a = IndexName::from_parts(&entity, &["a"]);
489 let ab = IndexName::from_parts(&entity, &["a", "b"]);
490 assert_eq!(a.cmp(&ab), a.to_bytes().cmp(&ab.to_bytes()));
491 }
492
493 #[test]
494 fn max_storable_orders_last() {
495 let entity = EntityName::from_static("zz");
496 let max = EntityName::max_storable();
497
498 assert!(entity < max);
499 }
500
501 fn gen_ascii(seed: u64, max_len: usize) -> String {
507 let len = (seed as usize % max_len).max(1);
508 let mut out = String::with_capacity(len);
509
510 let mut x = seed;
511 for _ in 0..len {
512 x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
514 let c = b'a' + (x % 26) as u8;
515 out.push(c as char);
516 }
517
518 out
519 }
520
521 #[test]
522 fn fuzz_entity_name_roundtrip_and_ordering() {
523 const RUNS: u64 = 1_000;
524
525 let mut prev: Option<EntityName> = None;
526
527 for i in 1..=RUNS {
528 let s = gen_ascii(i, MAX_ENTITY_NAME_LEN);
529 let e = EntityName::from_static(Box::leak(s.clone().into_boxed_str()));
530
531 let bytes = e.to_bytes();
533 let decoded = EntityName::from_bytes(&bytes).unwrap();
534 assert_eq!(e, decoded);
535
536 if let Some(p) = prev {
538 let ord_entity = p.cmp(&e);
539 let ord_bytes = p.to_bytes().cmp(&e.to_bytes());
540 assert_eq!(ord_entity, ord_bytes);
541 }
542
543 prev = Some(e);
544 }
545 }
546
547 #[test]
548 fn fuzz_index_name_roundtrip_and_ordering() {
549 const RUNS: u64 = 1_000;
550
551 let entity = EntityName::from_static("entity");
552 let mut prev: Option<IndexName> = None;
553
554 for i in 1..=RUNS {
555 let field_count = (i as usize % MAX_INDEX_FIELDS).max(1);
556
557 let mut field_strings = Vec::with_capacity(field_count);
558 let mut fields = Vec::with_capacity(field_count);
559 let mut string_parts = Vec::with_capacity(field_count + 1);
560
561 string_parts.push(entity.as_str().to_owned());
562
563 for f in 0..field_count {
564 let s = gen_ascii(i * 31 + f as u64, MAX_INDEX_FIELD_NAME_LEN);
565 string_parts.push(s.clone());
566 field_strings.push(s);
567 }
568
569 for s in &field_strings {
570 fields.push(s.as_str());
571 }
572
573 let idx = IndexName::from_parts(&entity, &fields);
574 let expected = string_parts.join("|");
575
576 assert_eq!(idx.as_str(), expected);
578
579 let bytes = idx.to_bytes();
581 let decoded = IndexName::from_bytes(&bytes).unwrap();
582 assert_eq!(idx, decoded);
583
584 if let Some(p_idx) = prev {
586 let ord_idx = p_idx.cmp(&idx);
587 let ord_bytes = p_idx.to_bytes().cmp(&idx.to_bytes());
588 assert_eq!(ord_idx, ord_bytes);
589 }
590
591 prev = Some(idx);
592 }
593 }
594}