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(u8::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_from_c_ptr_valid() {
494 let cstring = std::ffi::CString::new("hello").unwrap();
495 let s = unsafe { StackStr::from_c_ptr(cstring.as_ptr()) };
496 assert_eq!(s.as_str(), "hello");
497 }
498
499 #[rstest]
500 #[should_panic(expected = "Invalid UTF-8 in C string")]
501 fn test_from_c_ptr_invalid_utf8_panics() {
502 let bytes = vec![0xFF];
503 let cstring = unsafe { std::ffi::CString::from_vec_unchecked(bytes) };
504 let _ = unsafe { StackStr::from_c_ptr(cstring.as_ptr()) };
505 }
506
507 #[rstest]
508 #[should_panic(
509 expected = "Condition failed: String exceeds maximum length of 36 characters, was 37"
510 )]
511 fn test_from_c_ptr_too_long_panics() {
512 let long = "x".repeat(37);
513 let cstring = std::ffi::CString::new(long).unwrap();
514 let _ = unsafe { StackStr::from_c_ptr(cstring.as_ptr()) };
515 }
516
517 #[rstest]
518 fn test_equality() {
519 let a = StackStr::new("test");
520 let b = StackStr::new("test");
521 let c = StackStr::new("other");
522 assert_eq!(a, b);
523 assert_ne!(a, c);
524 }
525
526 #[rstest]
527 fn test_inequality_same_length() {
528 let a = StackStr::new("test");
529 let b = StackStr::new("tent");
530 assert_ne!(a, b);
531 }
532
533 #[rstest]
534 fn test_hash_consistency() {
535 use std::hash::DefaultHasher;
536
537 let a = StackStr::new("test");
538 let b = StackStr::new("test");
539
540 let hash_a = {
541 let mut h = DefaultHasher::new();
542 a.hash(&mut h);
543 h.finish()
544 };
545 let hash_b = {
546 let mut h = DefaultHasher::new();
547 b.hash(&mut h);
548 h.finish()
549 };
550
551 assert_eq!(hash_a, hash_b);
552 }
553
554 #[rstest]
555 fn test_hashmap_usage() {
556 let mut map = AHashMap::new();
557 map.insert(StackStr::new("key1"), 1);
558 map.insert(StackStr::new("key2"), 2);
559
560 assert_eq!(map.get(&StackStr::new("key1")), Some(&1));
561 assert_eq!(map.get(&StackStr::new("key2")), Some(&2));
562 assert_eq!(map.get(&StackStr::new("key3")), None);
563 }
564
565 #[rstest]
566 fn test_ordering() {
567 let a = StackStr::new("aaa");
568 let b = StackStr::new("bbb");
569 assert!(a < b);
570 assert!(b > a);
571 }
572
573 #[rstest]
574 fn test_c_compatibility() {
575 let s = StackStr::new("test");
576 let cstr = s.as_cstr();
577 assert_eq!(cstr.to_str().unwrap(), "test");
578 }
579
580 #[rstest]
581 fn test_as_ptr() {
582 let s = StackStr::new("test");
583 let ptr = s.as_ptr();
584 assert!(!ptr.is_null());
585
586 let cstr = unsafe { CStr::from_ptr(ptr) };
587 assert_eq!(cstr.to_str().unwrap(), "test");
588 }
589
590 #[rstest]
591 fn test_from_bytes() {
592 let s = StackStr::from_bytes(b"hello").unwrap();
593 assert_eq!(s.as_str(), "hello");
594 }
595
596 #[rstest]
597 fn test_from_bytes_with_null() {
598 let s = StackStr::from_bytes(b"hello\0").unwrap();
599 assert_eq!(s.as_str(), "hello");
600 }
601
602 #[rstest]
603 fn test_serde_roundtrip() {
604 let original = StackStr::new("test123");
605 let json = serde_json::to_string(&original).unwrap();
606 assert_eq!(json, "\"test123\"");
607
608 let deserialized: StackStr = serde_json::from_str(&json).unwrap();
609 assert_eq!(original, deserialized);
610 }
611
612 #[rstest]
613 fn test_display() {
614 let s = StackStr::new("hello");
615 assert_eq!(format!("{s}"), "hello");
616 }
617
618 #[rstest]
619 fn test_debug() {
620 let s = StackStr::new("hello");
621 assert_eq!(format!("{s:?}"), "\"hello\"");
622 }
623
624 #[rstest]
625 fn test_from_str() {
626 let s: StackStr = "hello".into();
627 assert_eq!(s.as_str(), "hello");
628 }
629
630 #[rstest]
631 fn test_as_ref() {
632 let s = StackStr::new("hello");
633 let r: &str = s.as_ref();
634 assert_eq!(r, "hello");
635 }
636
637 #[rstest]
638 fn test_borrow() {
639 let s = StackStr::new("hello");
640 let b: &str = s.borrow();
641 assert_eq!(b, "hello");
642 }
643
644 #[rstest]
645 fn test_default() {
646 let s = StackStr::default();
647 assert!(s.is_empty());
648 assert_eq!(s.len(), 0);
649 }
650
651 #[rstest]
652 fn test_copy_semantics() {
653 let a = StackStr::new("test");
654 let b = a; assert_eq!(a, b); }
657
658 #[rstest]
659 #[case("BINANCE")]
660 #[case("ETH-PERP")]
661 #[case("O-20231215-001")]
662 #[case("123456789012345678901234567890123456")] fn test_valid_identifiers(#[case] s: &str) {
664 let stack_str = StackStr::new(s);
665 assert_eq!(stack_str.as_str(), s);
666 }
667
668 #[rstest]
669 fn test_single_char() {
670 let s = StackStr::new("x");
671 assert_eq!(s.len(), 1);
672 assert_eq!(s.as_str(), "x");
673 }
674
675 #[rstest]
676 fn test_length_35() {
677 let input = "x".repeat(35);
678 let s = StackStr::new(&input);
679 assert_eq!(s.len(), 35);
680 }
681
682 #[rstest]
683 fn test_length_36_exact() {
684 let input = "x".repeat(36);
685 let s = StackStr::new(&input);
686 assert_eq!(s.len(), 36);
687 assert_eq!(s.as_str(), input);
688 }
689
690 #[rstest]
691 fn test_length_37_rejected() {
692 let input = "x".repeat(37);
693 let result = StackStr::new_checked(&input);
694 assert!(result.is_err());
695 assert!(result.unwrap_err().to_string().contains("exceeds"));
696 }
697
698 #[rstest]
699 fn test_struct_size() {
700 assert_eq!(std::mem::size_of::<StackStr>(), 38);
701 }
702
703 #[rstest]
704 fn test_value_field_at_offset_zero() {
705 let s = StackStr::new("hello");
706 let struct_ptr = std::ptr::from_ref(&s).cast::<u8>();
707 let first_byte = unsafe { *struct_ptr };
708 assert_eq!(first_byte, b'h');
709 }
710
711 #[rstest]
712 fn test_null_terminator_present() {
713 let s = StackStr::new("test");
714 let ptr = s.as_ptr();
715 let p = unsafe { ptr.add(4) };
717 let null_byte = unsafe { *p };
719 assert_eq!(null_byte, 0);
720 }
721
722 #[rstest]
723 fn test_from_bytes_empty() {
724 let result = StackStr::from_bytes(b"");
725 assert!(result.is_err());
726 }
727
728 #[rstest]
729 fn test_from_bytes_interior_nul() {
730 let result = StackStr::from_bytes(b"abc\0def");
731 assert!(result.is_err());
732 assert!(result.unwrap_err().to_string().contains("NUL"));
733 }
734
735 #[rstest]
736 fn test_from_bytes_non_ascii() {
737 let result = StackStr::from_bytes(&[0x80, 0x81]); assert!(result.is_err());
739 }
740
741 #[rstest]
742 fn test_from_bytes_too_long() {
743 let bytes = [b'x'; 55];
744 let result = StackStr::from_bytes(&bytes);
745 assert!(result.is_err());
746 }
747
748 #[rstest]
749 fn test_from_bytes_whitespace_only() {
750 let result = StackStr::from_bytes(b" ");
751 assert!(result.is_err());
752 }
753
754 #[rstest]
755 fn test_hash_differs_for_different_content() {
756 let a = StackStr::new("abc");
757 let b = StackStr::new("xyz");
758
759 let hash_a = {
760 let mut h = DefaultHasher::new();
761 a.hash(&mut h);
762 h.finish()
763 };
764 let hash_b = {
765 let mut h = DefaultHasher::new();
766 b.hash(&mut h);
767 h.finish()
768 };
769
770 assert_ne!(hash_a, hash_b);
771 }
772
773 #[rstest]
774 fn test_hash_ignores_padding() {
775 let a = StackStr::new("test");
776 let b = StackStr::new("test");
777
778 let hash_a = {
779 let mut h = DefaultHasher::new();
780 a.hash(&mut h);
781 h.finish()
782 };
783 let hash_b = {
784 let mut h = DefaultHasher::new();
785 b.hash(&mut h);
786 h.finish()
787 };
788
789 assert_eq!(hash_a, hash_b);
790 }
791
792 #[rstest]
793 fn test_serde_deserialize_too_long() {
794 let long = format!("\"{}\"", "x".repeat(55));
795 let result: Result<StackStr, _> = serde_json::from_str(&long);
796 assert!(result.is_err());
797 }
798
799 #[rstest]
800 fn test_serde_deserialize_empty() {
801 let result: Result<StackStr, _> = serde_json::from_str("\"\"");
802 assert!(result.is_err());
803 }
804
805 #[rstest]
806 fn test_serde_deserialize_non_ascii() {
807 let result: Result<StackStr, _> = serde_json::from_str("\"hello\u{1F600}\"");
808 assert!(result.is_err());
809 }
810
811 #[rstest]
812 #[case("!@#$%^&*()")]
813 #[case("hello-world_123")]
814 #[case("a.b.c.d")]
815 #[case("key=value")]
816 #[case("path/to/file")]
817 #[case("[bracket]")]
818 #[case("{curly}")]
819 fn test_special_ascii_chars(#[case] s: &str) {
820 let stack_str = StackStr::new(s);
821 assert_eq!(stack_str.as_str(), s);
822 }
823
824 #[rstest]
825 fn test_ascii_control_chars_tab() {
826 let result = StackStr::new_checked("a\tb");
828 assert!(result.is_ok());
829 assert_eq!(result.unwrap().as_str(), "a\tb");
830 }
831
832 #[rstest]
833 fn test_ordering_same_prefix_different_length() {
834 let short = StackStr::new("abc");
835 let long = StackStr::new("abcd");
836 assert!(short < long);
837 }
838
839 #[rstest]
840 fn test_ordering_case_sensitive() {
841 let upper = StackStr::new("ABC");
842 let lower = StackStr::new("abc");
843 assert!(upper < lower);
845 }
846
847 #[rstest]
848 fn test_partial_cmp_returns_some() {
849 let a = StackStr::new("test");
850 let b = StackStr::new("test");
851 assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Equal));
852 }
853
854 #[rstest]
855 fn test_new_checked_error_empty() {
856 let err = StackStr::new_checked("").unwrap_err();
857 assert!(err.to_string().contains("empty"));
858 }
859
860 #[rstest]
861 fn test_new_checked_error_whitespace() {
862 let err = StackStr::new_checked(" ").unwrap_err();
863 assert!(err.to_string().contains("whitespace"));
864 }
865
866 #[rstest]
867 fn test_new_checked_error_too_long() {
868 let err = StackStr::new_checked(&"x".repeat(55)).unwrap_err();
869 assert!(err.to_string().contains("exceeds"));
870 }
871
872 #[rstest]
873 fn test_new_checked_error_non_ascii() {
874 let err = StackStr::new_checked("hello\u{1F600}").unwrap_err();
875 assert!(err.to_string().contains("non-ASCII"));
876 }
877
878 #[rstest]
879 fn test_new_checked_error_interior_nul() {
880 let err = StackStr::new_checked("abc\0def").unwrap_err();
881 assert!(err.to_string().contains("NUL"));
882 }
883
884 #[rstest]
885 fn test_clone_equals_original() {
886 let a = StackStr::new("test");
887 #[expect(clippy::clone_on_copy)]
888 let b = a.clone();
889 assert_eq!(a, b);
890 }
891
892 #[rstest]
893 fn test_deref() {
894 let s = StackStr::new("hello");
895 assert!(s.starts_with("hell"));
896 assert_eq!(s.len(), 5);
897 }
898
899 #[rstest]
900 fn test_partial_eq_str_literal() {
901 let s = StackStr::new("hello");
902 assert_eq!(s, "hello");
903 assert!(s != "world");
904 }
905
906 #[rstest]
907 fn test_partial_eq_str_unsized() {
908 let s = StackStr::new("hello");
909 assert_eq!(s, *"hello");
910 assert!(s != *"world");
911 }
912
913 #[rstest]
914 fn test_try_from_bytes() {
915 let s: StackStr = b"hello".as_slice().try_into().unwrap();
916 assert_eq!(s.as_str(), "hello");
917 }
918}