1#![allow(unsafe_code)]
41
42use std::{
43 borrow::Borrow,
44 cmp::Ordering,
45 ffi::{CStr, c_char},
46 fmt::{Debug, Display},
47 hash::{Hash, Hasher},
48 ops::Deref,
49};
50
51use serde::{Deserialize, Deserializer, Serialize, Serializer};
52
53use crate::correctness::{CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED};
54
55pub const STACKSTR_CAPACITY: usize = 36;
57
58const STACKSTR_BUFFER_SIZE: usize = STACKSTR_CAPACITY + 1;
60
61#[derive(Clone, Copy)]
78#[repr(C)]
79pub struct StackStr {
80 value: [u8; 37], len: u8,
84}
85
86impl StackStr {
87 pub const MAX_LEN: usize = STACKSTR_CAPACITY;
89
90 #[must_use]
99 pub fn new(s: &str) -> Self {
100 Self::new_checked(s).expect_display(FAILED)
101 }
102
103 #[expect(
112 clippy::cast_possible_truncation,
113 reason = "length is guarded by STACKSTR_CAPACITY check above (max 36, fits u8)"
114 )]
115 pub fn new_checked(s: &str) -> CorrectnessResult<Self> {
116 if s.is_empty() {
117 return Err(CorrectnessError::PredicateViolation {
118 message: "String is empty".to_string(),
119 });
120 }
121
122 if s.len() > STACKSTR_CAPACITY {
123 return Err(CorrectnessError::PredicateViolation {
124 message: format!(
125 "String exceeds maximum length of {} characters, was {}",
126 STACKSTR_CAPACITY,
127 s.len()
128 ),
129 });
130 }
131
132 if !s.is_ascii() {
133 return Err(CorrectnessError::PredicateViolation {
134 message: "String contains non-ASCII character".to_string(),
135 });
136 }
137
138 let bytes = s.as_bytes();
139 if bytes.contains(&0) {
140 return Err(CorrectnessError::PredicateViolation {
141 message: "String contains interior NUL byte".to_string(),
142 });
143 }
144
145 if bytes.iter().all(|b| b.is_ascii_whitespace()) {
146 return Err(CorrectnessError::PredicateViolation {
147 message: "String contains only whitespace".to_string(),
148 });
149 }
150
151 let mut value = [0u8; STACKSTR_BUFFER_SIZE];
152 value[..s.len()].copy_from_slice(bytes);
153 Ok(Self {
156 value,
157 len: s.len() as u8,
158 })
159 }
160
161 pub fn from_bytes(bytes: &[u8]) -> CorrectnessResult<Self> {
170 let bytes = if bytes.last() == Some(&0) {
172 &bytes[..bytes.len() - 1]
173 } else {
174 bytes
175 };
176
177 let s = std::str::from_utf8(bytes).map_err(|e| CorrectnessError::PredicateViolation {
178 message: format!("Invalid UTF-8: {e}"),
179 })?;
180
181 Self::new_checked(s)
182 }
183
184 #[must_use]
203 pub unsafe fn from_c_ptr(ptr: *const c_char) -> Self {
204 let cstr = unsafe { CStr::from_ptr(ptr) };
206 let s = cstr.to_str().expect("Invalid UTF-8 in C string");
207 Self::new(s)
208 }
209
210 #[must_use]
219 pub unsafe fn from_c_ptr_checked(ptr: *const c_char) -> Option<Self> {
220 if ptr.is_null() {
221 return None;
222 }
223
224 let cstr = unsafe { CStr::from_ptr(ptr) };
226 let s = cstr.to_str().ok()?;
227 Self::new_checked(s).ok()
228 }
229
230 #[inline]
234 #[must_use]
235 pub fn as_str(&self) -> &str {
236 debug_assert!(
237 self.len as usize <= STACKSTR_CAPACITY,
238 "StackStr len {} exceeds capacity {}",
239 self.len,
240 STACKSTR_CAPACITY
241 );
242 unsafe { std::str::from_utf8_unchecked(&self.value[..self.len as usize]) }
245 }
246
247 #[inline]
251 #[must_use]
252 pub const fn len(&self) -> usize {
253 self.len as usize
254 }
255
256 #[inline]
258 #[must_use]
259 pub const fn is_empty(&self) -> bool {
260 self.len == 0
261 }
262
263 #[inline]
265 #[must_use]
266 pub const fn as_ptr(&self) -> *const c_char {
267 self.value.as_ptr().cast::<c_char>()
268 }
269
270 #[inline]
272 #[must_use]
273 pub fn as_cstr(&self) -> &CStr {
274 debug_assert!(
275 self.len as usize <= STACKSTR_CAPACITY,
276 "StackStr len {} exceeds capacity {}",
277 self.len,
278 STACKSTR_CAPACITY
279 );
280 debug_assert!(
281 self.value[self.len as usize] == 0,
282 "StackStr missing null terminator at position {}",
283 self.len
284 );
285 unsafe { CStr::from_bytes_with_nul_unchecked(&self.value[..=self.len as usize]) }
289 }
290}
291
292impl PartialEq for StackStr {
293 #[inline]
294 fn eq(&self, other: &Self) -> bool {
295 self.len == other.len
296 && self.value[..self.len as usize] == other.value[..other.len as usize]
297 }
298}
299
300impl Eq for StackStr {}
301
302impl Hash for StackStr {
303 #[inline]
304 fn hash<H: Hasher>(&self, state: &mut H) {
305 self.value[..self.len as usize].hash(state);
307 }
308}
309
310impl Ord for StackStr {
311 fn cmp(&self, other: &Self) -> Ordering {
312 self.as_str().cmp(other.as_str())
313 }
314}
315
316impl PartialOrd for StackStr {
317 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
318 Some(self.cmp(other))
319 }
320}
321
322impl Display for StackStr {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 f.write_str(self.as_str())
325 }
326}
327
328impl Debug for StackStr {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 write!(f, "{:?}", self.as_str())
331 }
332}
333
334impl Serialize for StackStr {
335 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
336 serializer.serialize_str(self.as_str())
337 }
338}
339
340impl<'de> Deserialize<'de> for StackStr {
341 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
342 let s: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
343 Self::new_checked(s.as_ref()).map_err(serde::de::Error::custom)
344 }
345}
346
347impl From<&str> for StackStr {
348 fn from(s: &str) -> Self {
349 Self::new(s)
350 }
351}
352
353impl AsRef<str> for StackStr {
354 fn as_ref(&self) -> &str {
355 self.as_str()
356 }
357}
358
359impl Borrow<str> for StackStr {
360 fn borrow(&self) -> &str {
361 self.as_str()
362 }
363}
364
365impl Default for StackStr {
366 fn default() -> Self {
371 Self {
372 value: [0u8; STACKSTR_BUFFER_SIZE],
373 len: 0,
374 }
375 }
376}
377
378impl Deref for StackStr {
379 type Target = str;
380
381 fn deref(&self) -> &Self::Target {
382 self.as_str()
383 }
384}
385
386impl PartialEq<&str> for StackStr {
387 fn eq(&self, other: &&str) -> bool {
388 self.as_str() == *other
389 }
390}
391
392impl PartialEq<str> for StackStr {
393 fn eq(&self, other: &str) -> bool {
394 self.as_str() == other
395 }
396}
397
398impl TryFrom<&[u8]> for StackStr {
399 type Error = CorrectnessError;
400
401 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
402 Self::from_bytes(bytes)
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use std::hash::{DefaultHasher, Hasher};
409
410 use ahash::AHashMap;
411 use rstest::rstest;
412
413 use super::*;
414
415 #[rstest]
416 fn test_new_valid() {
417 let s = StackStr::new("hello");
418 assert_eq!(s.as_str(), "hello");
419 assert_eq!(s.len(), 5);
420 assert!(!s.is_empty());
421 }
422
423 #[rstest]
424 fn test_max_length() {
425 let input = "x".repeat(36);
426 let s = StackStr::new(&input);
427 assert_eq!(s.len(), 36);
428 assert_eq!(s.as_str(), input);
429 }
430
431 #[rstest]
432 #[should_panic(expected = "Condition failed")]
433 fn test_exceeds_max_length() {
434 let input = "x".repeat(37);
435 let _ = StackStr::new(&input);
436 }
437
438 #[rstest]
439 #[should_panic(expected = "Condition failed")]
440 fn test_empty_string() {
441 let _ = StackStr::new("");
442 }
443
444 #[rstest]
445 #[should_panic(expected = "Condition failed")]
446 fn test_whitespace_only() {
447 let _ = StackStr::new(" ");
448 }
449
450 #[rstest]
451 #[should_panic(expected = "Condition failed")]
452 fn test_non_ascii() {
453 let _ = StackStr::new("hello\u{1F600}"); }
455
456 #[rstest]
457 #[should_panic(expected = "Condition failed")]
458 fn test_interior_nul_byte() {
459 let _ = StackStr::new("abc\0def");
460 }
461
462 #[rstest]
463 fn test_interior_nul_byte_checked() {
464 let result = StackStr::new_checked("abc\0def");
465 assert!(result.is_err());
466 assert!(result.unwrap_err().to_string().contains("NUL"));
467 }
468
469 #[rstest]
470 fn test_from_c_ptr_checked_valid() {
471 let cstring = std::ffi::CString::new("hello").unwrap();
472 let s = unsafe { StackStr::from_c_ptr_checked(cstring.as_ptr()) };
473 assert!(s.is_some());
474 assert_eq!(s.unwrap().as_str(), "hello");
475 }
476
477 #[rstest]
478 fn test_from_c_ptr_checked_too_long() {
479 let long = "x".repeat(37);
480 let cstring = std::ffi::CString::new(long).unwrap();
481 let s = unsafe { StackStr::from_c_ptr_checked(cstring.as_ptr()) };
482 assert!(s.is_none());
483 }
484
485 #[rstest]
486 fn test_from_c_ptr_checked_null() {
487 let s = unsafe { StackStr::from_c_ptr_checked(std::ptr::null()) };
488
489 assert!(s.is_none());
490 }
491
492 #[rstest]
493 fn test_equality() {
494 let a = StackStr::new("test");
495 let b = StackStr::new("test");
496 let c = StackStr::new("other");
497 assert_eq!(a, b);
498 assert_ne!(a, c);
499 }
500
501 #[rstest]
502 fn test_hash_consistency() {
503 use std::hash::DefaultHasher;
504
505 let a = StackStr::new("test");
506 let b = StackStr::new("test");
507
508 let hash_a = {
509 let mut h = DefaultHasher::new();
510 a.hash(&mut h);
511 h.finish()
512 };
513 let hash_b = {
514 let mut h = DefaultHasher::new();
515 b.hash(&mut h);
516 h.finish()
517 };
518
519 assert_eq!(hash_a, hash_b);
520 }
521
522 #[rstest]
523 fn test_hashmap_usage() {
524 let mut map = AHashMap::new();
525 map.insert(StackStr::new("key1"), 1);
526 map.insert(StackStr::new("key2"), 2);
527
528 assert_eq!(map.get(&StackStr::new("key1")), Some(&1));
529 assert_eq!(map.get(&StackStr::new("key2")), Some(&2));
530 assert_eq!(map.get(&StackStr::new("key3")), None);
531 }
532
533 #[rstest]
534 fn test_ordering() {
535 let a = StackStr::new("aaa");
536 let b = StackStr::new("bbb");
537 assert!(a < b);
538 assert!(b > a);
539 }
540
541 #[rstest]
542 fn test_c_compatibility() {
543 let s = StackStr::new("test");
544 let cstr = s.as_cstr();
545 assert_eq!(cstr.to_str().unwrap(), "test");
546 }
547
548 #[rstest]
549 fn test_as_ptr() {
550 let s = StackStr::new("test");
551 let ptr = s.as_ptr();
552 assert!(!ptr.is_null());
553
554 let cstr = unsafe { CStr::from_ptr(ptr) };
555 assert_eq!(cstr.to_str().unwrap(), "test");
556 }
557
558 #[rstest]
559 fn test_from_bytes() {
560 let s = StackStr::from_bytes(b"hello").unwrap();
561 assert_eq!(s.as_str(), "hello");
562 }
563
564 #[rstest]
565 fn test_from_bytes_with_null() {
566 let s = StackStr::from_bytes(b"hello\0").unwrap();
567 assert_eq!(s.as_str(), "hello");
568 }
569
570 #[rstest]
571 fn test_serde_roundtrip() {
572 let original = StackStr::new("test123");
573 let json = serde_json::to_string(&original).unwrap();
574 assert_eq!(json, "\"test123\"");
575
576 let deserialized: StackStr = serde_json::from_str(&json).unwrap();
577 assert_eq!(original, deserialized);
578 }
579
580 #[rstest]
581 fn test_display() {
582 let s = StackStr::new("hello");
583 assert_eq!(format!("{s}"), "hello");
584 }
585
586 #[rstest]
587 fn test_debug() {
588 let s = StackStr::new("hello");
589 assert_eq!(format!("{s:?}"), "\"hello\"");
590 }
591
592 #[rstest]
593 fn test_from_str() {
594 let s: StackStr = "hello".into();
595 assert_eq!(s.as_str(), "hello");
596 }
597
598 #[rstest]
599 fn test_as_ref() {
600 let s = StackStr::new("hello");
601 let r: &str = s.as_ref();
602 assert_eq!(r, "hello");
603 }
604
605 #[rstest]
606 fn test_borrow() {
607 let s = StackStr::new("hello");
608 let b: &str = s.borrow();
609 assert_eq!(b, "hello");
610 }
611
612 #[rstest]
613 fn test_default() {
614 let s = StackStr::default();
615 assert!(s.is_empty());
616 assert_eq!(s.len(), 0);
617 }
618
619 #[rstest]
620 fn test_copy_semantics() {
621 let a = StackStr::new("test");
622 let b = a; assert_eq!(a, b); }
625
626 #[rstest]
627 #[case("BINANCE")]
628 #[case("ETH-PERP")]
629 #[case("O-20231215-001")]
630 #[case("123456789012345678901234567890123456")] fn test_valid_identifiers(#[case] s: &str) {
632 let stack_str = StackStr::new(s);
633 assert_eq!(stack_str.as_str(), s);
634 }
635
636 #[rstest]
637 fn test_single_char() {
638 let s = StackStr::new("x");
639 assert_eq!(s.len(), 1);
640 assert_eq!(s.as_str(), "x");
641 }
642
643 #[rstest]
644 fn test_length_35() {
645 let input = "x".repeat(35);
646 let s = StackStr::new(&input);
647 assert_eq!(s.len(), 35);
648 }
649
650 #[rstest]
651 fn test_length_36_exact() {
652 let input = "x".repeat(36);
653 let s = StackStr::new(&input);
654 assert_eq!(s.len(), 36);
655 assert_eq!(s.as_str(), input);
656 }
657
658 #[rstest]
659 fn test_length_37_rejected() {
660 let input = "x".repeat(37);
661 let result = StackStr::new_checked(&input);
662 assert!(result.is_err());
663 assert!(result.unwrap_err().to_string().contains("exceeds"));
664 }
665
666 #[rstest]
667 fn test_struct_size() {
668 assert_eq!(std::mem::size_of::<StackStr>(), 38);
669 }
670
671 #[rstest]
672 fn test_value_field_at_offset_zero() {
673 let s = StackStr::new("hello");
674 let struct_ptr = std::ptr::from_ref(&s).cast::<u8>();
675 let first_byte = unsafe { *struct_ptr };
676 assert_eq!(first_byte, b'h');
677 }
678
679 #[rstest]
680 fn test_null_terminator_present() {
681 let s = StackStr::new("test");
682 let ptr = s.as_ptr();
683 let p = unsafe { ptr.add(4) };
685 let null_byte = unsafe { *p };
687 assert_eq!(null_byte, 0);
688 }
689
690 #[rstest]
691 fn test_from_bytes_empty() {
692 let result = StackStr::from_bytes(b"");
693 assert!(result.is_err());
694 }
695
696 #[rstest]
697 fn test_from_bytes_interior_nul() {
698 let result = StackStr::from_bytes(b"abc\0def");
699 assert!(result.is_err());
700 assert!(result.unwrap_err().to_string().contains("NUL"));
701 }
702
703 #[rstest]
704 fn test_from_bytes_non_ascii() {
705 let result = StackStr::from_bytes(&[0x80, 0x81]); assert!(result.is_err());
707 }
708
709 #[rstest]
710 fn test_from_bytes_too_long() {
711 let bytes = [b'x'; 55];
712 let result = StackStr::from_bytes(&bytes);
713 assert!(result.is_err());
714 }
715
716 #[rstest]
717 fn test_from_bytes_whitespace_only() {
718 let result = StackStr::from_bytes(b" ");
719 assert!(result.is_err());
720 }
721
722 #[rstest]
723 fn test_hash_differs_for_different_content() {
724 let a = StackStr::new("abc");
725 let b = StackStr::new("xyz");
726
727 let hash_a = {
728 let mut h = DefaultHasher::new();
729 a.hash(&mut h);
730 h.finish()
731 };
732 let hash_b = {
733 let mut h = DefaultHasher::new();
734 b.hash(&mut h);
735 h.finish()
736 };
737
738 assert_ne!(hash_a, hash_b);
739 }
740
741 #[rstest]
742 fn test_hash_ignores_padding() {
743 let a = StackStr::new("test");
744 let b = StackStr::new("test");
745
746 let hash_a = {
747 let mut h = DefaultHasher::new();
748 a.hash(&mut h);
749 h.finish()
750 };
751 let hash_b = {
752 let mut h = DefaultHasher::new();
753 b.hash(&mut h);
754 h.finish()
755 };
756
757 assert_eq!(hash_a, hash_b);
758 }
759
760 #[rstest]
761 fn test_serde_deserialize_too_long() {
762 let long = format!("\"{}\"", "x".repeat(55));
763 let result: Result<StackStr, _> = serde_json::from_str(&long);
764 assert!(result.is_err());
765 }
766
767 #[rstest]
768 fn test_serde_deserialize_empty() {
769 let result: Result<StackStr, _> = serde_json::from_str("\"\"");
770 assert!(result.is_err());
771 }
772
773 #[rstest]
774 fn test_serde_deserialize_non_ascii() {
775 let result: Result<StackStr, _> = serde_json::from_str("\"hello\u{1F600}\"");
776 assert!(result.is_err());
777 }
778
779 #[rstest]
780 #[case("!@#$%^&*()")]
781 #[case("hello-world_123")]
782 #[case("a.b.c.d")]
783 #[case("key=value")]
784 #[case("path/to/file")]
785 #[case("[bracket]")]
786 #[case("{curly}")]
787 fn test_special_ascii_chars(#[case] s: &str) {
788 let stack_str = StackStr::new(s);
789 assert_eq!(stack_str.as_str(), s);
790 }
791
792 #[rstest]
793 fn test_ascii_control_chars_tab() {
794 let result = StackStr::new_checked("a\tb");
796 assert!(result.is_ok());
797 assert_eq!(result.unwrap().as_str(), "a\tb");
798 }
799
800 #[rstest]
801 fn test_ordering_same_prefix_different_length() {
802 let short = StackStr::new("abc");
803 let long = StackStr::new("abcd");
804 assert!(short < long);
805 }
806
807 #[rstest]
808 fn test_ordering_case_sensitive() {
809 let upper = StackStr::new("ABC");
810 let lower = StackStr::new("abc");
811 assert!(upper < lower);
813 }
814
815 #[rstest]
816 fn test_partial_cmp_returns_some() {
817 let a = StackStr::new("test");
818 let b = StackStr::new("test");
819 assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Equal));
820 }
821
822 #[rstest]
823 fn test_new_checked_error_empty() {
824 let err = StackStr::new_checked("").unwrap_err();
825 assert!(err.to_string().contains("empty"));
826 }
827
828 #[rstest]
829 fn test_new_checked_error_whitespace() {
830 let err = StackStr::new_checked(" ").unwrap_err();
831 assert!(err.to_string().contains("whitespace"));
832 }
833
834 #[rstest]
835 fn test_new_checked_error_too_long() {
836 let err = StackStr::new_checked(&"x".repeat(55)).unwrap_err();
837 assert!(err.to_string().contains("exceeds"));
838 }
839
840 #[rstest]
841 fn test_new_checked_error_non_ascii() {
842 let err = StackStr::new_checked("hello\u{1F600}").unwrap_err();
843 assert!(err.to_string().contains("non-ASCII"));
844 }
845
846 #[rstest]
847 fn test_new_checked_error_interior_nul() {
848 let err = StackStr::new_checked("abc\0def").unwrap_err();
849 assert!(err.to_string().contains("NUL"));
850 }
851
852 #[rstest]
853 fn test_clone_equals_original() {
854 let a = StackStr::new("test");
855 #[expect(clippy::clone_on_copy)]
856 let b = a.clone();
857 assert_eq!(a, b);
858 }
859
860 #[rstest]
861 fn test_deref() {
862 let s = StackStr::new("hello");
863 assert!(s.starts_with("hell"));
864 assert_eq!(s.len(), 5);
865 }
866
867 #[rstest]
868 fn test_partial_eq_str_literal() {
869 let s = StackStr::new("hello");
870 assert_eq!(s, "hello");
871 assert!(s != "world");
872 }
873
874 #[rstest]
875 fn test_try_from_bytes() {
876 let s: StackStr = b"hello".as_slice().try_into().unwrap();
877 assert_eq!(s.as_str(), "hello");
878 }
879}