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.as_str().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, AHashSet};
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 #[case("key1")]
556 #[case("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")]
557 fn test_hash_matches_borrowed_str(#[case] text: &str) {
558 let value = StackStr::new(text);
559 let mut owned_hasher = DefaultHasher::new();
560 let mut borrowed_hasher = DefaultHasher::new();
561
562 value.hash(&mut owned_hasher);
563 text.hash(&mut borrowed_hasher);
564
565 assert_eq!(owned_hasher.finish(), borrowed_hasher.finish());
566 }
567
568 #[rstest]
569 fn test_hashmap_usage() {
570 let mut map = AHashMap::new();
571 map.insert(StackStr::new("key1"), 1);
572 map.insert(StackStr::new("key2"), 2);
573
574 assert_eq!(map.get(&StackStr::new("key1")), Some(&1));
575 assert_eq!(map.get(&StackStr::new("key2")), Some(&2));
576 assert_eq!(map.get(&StackStr::new("key3")), None);
577 assert_eq!(map.get("key1"), Some(&1));
578 assert_eq!(map.get("key2"), Some(&2));
579 assert_eq!(map.get("key3"), None);
580 }
581
582 #[rstest]
583 fn test_hashset_borrowed_lookup() {
584 let set = AHashSet::from([StackStr::new("key1"), StackStr::new("key2")]);
585
586 assert!(set.contains("key1"));
587 assert!(set.contains("key2"));
588 assert!(!set.contains("key3"));
589 }
590
591 #[rstest]
592 fn test_ordering() {
593 let a = StackStr::new("aaa");
594 let b = StackStr::new("bbb");
595 assert!(a < b);
596 assert!(b > a);
597 }
598
599 #[rstest]
600 fn test_c_compatibility() {
601 let s = StackStr::new("test");
602 let cstr = s.as_cstr();
603 assert_eq!(cstr.to_str().unwrap(), "test");
604 }
605
606 #[rstest]
607 fn test_as_ptr() {
608 let s = StackStr::new("test");
609 let ptr = s.as_ptr();
610 assert!(!ptr.is_null());
611
612 let cstr = unsafe { CStr::from_ptr(ptr) };
613 assert_eq!(cstr.to_str().unwrap(), "test");
614 }
615
616 #[rstest]
617 fn test_from_bytes() {
618 let s = StackStr::from_bytes(b"hello").unwrap();
619 assert_eq!(s.as_str(), "hello");
620 }
621
622 #[rstest]
623 fn test_from_bytes_with_null() {
624 let s = StackStr::from_bytes(b"hello\0").unwrap();
625 assert_eq!(s.as_str(), "hello");
626 }
627
628 #[rstest]
629 fn test_serde_roundtrip() {
630 let original = StackStr::new("test123");
631 let json = serde_json::to_string(&original).unwrap();
632 assert_eq!(json, "\"test123\"");
633
634 let deserialized: StackStr = serde_json::from_str(&json).unwrap();
635 assert_eq!(original, deserialized);
636 }
637
638 #[rstest]
639 fn test_display() {
640 let s = StackStr::new("hello");
641 assert_eq!(format!("{s}"), "hello");
642 }
643
644 #[rstest]
645 fn test_debug() {
646 let s = StackStr::new("hello");
647 assert_eq!(format!("{s:?}"), "\"hello\"");
648 }
649
650 #[rstest]
651 fn test_from_str() {
652 let s: StackStr = "hello".into();
653 assert_eq!(s.as_str(), "hello");
654 }
655
656 #[rstest]
657 fn test_as_ref() {
658 let s = StackStr::new("hello");
659 let r: &str = s.as_ref();
660 assert_eq!(r, "hello");
661 }
662
663 #[rstest]
664 fn test_borrow() {
665 let s = StackStr::new("hello");
666 let b: &str = s.borrow();
667 assert_eq!(b, "hello");
668 }
669
670 #[rstest]
671 fn test_default() {
672 let s = StackStr::default();
673 assert!(s.is_empty());
674 assert_eq!(s.len(), 0);
675 }
676
677 #[rstest]
678 fn test_copy_semantics() {
679 let a = StackStr::new("test");
680 let b = a; assert_eq!(a, b); }
683
684 #[rstest]
685 #[case("BINANCE")]
686 #[case("ETH-PERP")]
687 #[case("O-20231215-001")]
688 #[case("123456789012345678901234567890123456")] fn test_valid_identifiers(#[case] s: &str) {
690 let stack_str = StackStr::new(s);
691 assert_eq!(stack_str.as_str(), s);
692 }
693
694 #[rstest]
695 fn test_single_char() {
696 let s = StackStr::new("x");
697 assert_eq!(s.len(), 1);
698 assert_eq!(s.as_str(), "x");
699 }
700
701 #[rstest]
702 fn test_length_35() {
703 let input = "x".repeat(35);
704 let s = StackStr::new(&input);
705 assert_eq!(s.len(), 35);
706 }
707
708 #[rstest]
709 fn test_length_36_exact() {
710 let input = "x".repeat(36);
711 let s = StackStr::new(&input);
712 assert_eq!(s.len(), 36);
713 assert_eq!(s.as_str(), input);
714 }
715
716 #[rstest]
717 fn test_length_37_rejected() {
718 let input = "x".repeat(37);
719 let result = StackStr::new_checked(&input);
720 assert!(result.is_err());
721 assert!(result.unwrap_err().to_string().contains("exceeds"));
722 }
723
724 #[rstest]
725 fn test_struct_size() {
726 assert_eq!(std::mem::size_of::<StackStr>(), 38);
727 }
728
729 #[rstest]
730 fn test_value_field_at_offset_zero() {
731 let s = StackStr::new("hello");
732 let struct_ptr = std::ptr::from_ref(&s).cast::<u8>();
733 let first_byte = unsafe { *struct_ptr };
734 assert_eq!(first_byte, b'h');
735 }
736
737 #[rstest]
738 fn test_null_terminator_present() {
739 let s = StackStr::new("test");
740 let ptr = s.as_ptr();
741 let p = unsafe { ptr.add(4) };
743 let null_byte = unsafe { *p };
745 assert_eq!(null_byte, 0);
746 }
747
748 #[rstest]
749 fn test_from_bytes_empty() {
750 let result = StackStr::from_bytes(b"");
751 assert!(result.is_err());
752 }
753
754 #[rstest]
755 fn test_from_bytes_interior_nul() {
756 let result = StackStr::from_bytes(b"abc\0def");
757 assert!(result.is_err());
758 assert!(result.unwrap_err().to_string().contains("NUL"));
759 }
760
761 #[rstest]
762 fn test_from_bytes_non_ascii() {
763 let result = StackStr::from_bytes(&[0x80, 0x81]); assert!(result.is_err());
765 }
766
767 #[rstest]
768 fn test_from_bytes_too_long() {
769 let bytes = [b'x'; 55];
770 let result = StackStr::from_bytes(&bytes);
771 assert!(result.is_err());
772 }
773
774 #[rstest]
775 fn test_from_bytes_whitespace_only() {
776 let result = StackStr::from_bytes(b" ");
777 assert!(result.is_err());
778 }
779
780 #[rstest]
781 fn test_hash_differs_for_different_content() {
782 let a = StackStr::new("abc");
783 let b = StackStr::new("xyz");
784
785 let hash_a = {
786 let mut h = DefaultHasher::new();
787 a.hash(&mut h);
788 h.finish()
789 };
790 let hash_b = {
791 let mut h = DefaultHasher::new();
792 b.hash(&mut h);
793 h.finish()
794 };
795
796 assert_ne!(hash_a, hash_b);
797 }
798
799 #[rstest]
800 fn test_hash_ignores_padding() {
801 let a = StackStr::new("test");
802 let b = StackStr::new("test");
803
804 let hash_a = {
805 let mut h = DefaultHasher::new();
806 a.hash(&mut h);
807 h.finish()
808 };
809 let hash_b = {
810 let mut h = DefaultHasher::new();
811 b.hash(&mut h);
812 h.finish()
813 };
814
815 assert_eq!(hash_a, hash_b);
816 }
817
818 #[rstest]
819 fn test_serde_deserialize_too_long() {
820 let long = format!("\"{}\"", "x".repeat(55));
821 let result: Result<StackStr, _> = serde_json::from_str(&long);
822 assert!(result.is_err());
823 }
824
825 #[rstest]
826 fn test_serde_deserialize_empty() {
827 let result: Result<StackStr, _> = serde_json::from_str("\"\"");
828 assert!(result.is_err());
829 }
830
831 #[rstest]
832 fn test_serde_deserialize_non_ascii() {
833 let result: Result<StackStr, _> = serde_json::from_str("\"hello\u{1F600}\"");
834 assert!(result.is_err());
835 }
836
837 #[rstest]
838 #[case("!@#$%^&*()")]
839 #[case("hello-world_123")]
840 #[case("a.b.c.d")]
841 #[case("key=value")]
842 #[case("path/to/file")]
843 #[case("[bracket]")]
844 #[case("{curly}")]
845 fn test_special_ascii_chars(#[case] s: &str) {
846 let stack_str = StackStr::new(s);
847 assert_eq!(stack_str.as_str(), s);
848 }
849
850 #[rstest]
851 fn test_ascii_control_chars_tab() {
852 let result = StackStr::new_checked("a\tb");
854 assert!(result.is_ok());
855 assert_eq!(result.unwrap().as_str(), "a\tb");
856 }
857
858 #[rstest]
859 fn test_ordering_same_prefix_different_length() {
860 let short = StackStr::new("abc");
861 let long = StackStr::new("abcd");
862 assert!(short < long);
863 }
864
865 #[rstest]
866 fn test_ordering_case_sensitive() {
867 let upper = StackStr::new("ABC");
868 let lower = StackStr::new("abc");
869 assert!(upper < lower);
871 }
872
873 #[rstest]
874 fn test_partial_cmp_returns_some() {
875 let a = StackStr::new("test");
876 let b = StackStr::new("test");
877 assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Equal));
878 }
879
880 #[rstest]
881 fn test_new_checked_error_empty() {
882 let err = StackStr::new_checked("").unwrap_err();
883 assert!(err.to_string().contains("empty"));
884 }
885
886 #[rstest]
887 fn test_new_checked_error_whitespace() {
888 let err = StackStr::new_checked(" ").unwrap_err();
889 assert!(err.to_string().contains("whitespace"));
890 }
891
892 #[rstest]
893 fn test_new_checked_error_too_long() {
894 let err = StackStr::new_checked(&"x".repeat(55)).unwrap_err();
895 assert!(err.to_string().contains("exceeds"));
896 }
897
898 #[rstest]
899 fn test_new_checked_error_non_ascii() {
900 let err = StackStr::new_checked("hello\u{1F600}").unwrap_err();
901 assert!(err.to_string().contains("non-ASCII"));
902 }
903
904 #[rstest]
905 fn test_new_checked_error_interior_nul() {
906 let err = StackStr::new_checked("abc\0def").unwrap_err();
907 assert!(err.to_string().contains("NUL"));
908 }
909
910 #[rstest]
911 fn test_clone_equals_original() {
912 let a = StackStr::new("test");
913 #[expect(clippy::clone_on_copy)]
914 let b = a.clone();
915 assert_eq!(a, b);
916 }
917
918 #[rstest]
919 fn test_deref() {
920 let s = StackStr::new("hello");
921 assert!(s.starts_with("hell"));
922 assert_eq!(s.len(), 5);
923 }
924
925 #[rstest]
926 fn test_partial_eq_str_literal() {
927 let s = StackStr::new("hello");
928 assert_eq!(s, "hello");
929 assert!(s != "world");
930 }
931
932 #[rstest]
933 fn test_partial_eq_str_unsized() {
934 let s = StackStr::new("hello");
935 assert_eq!(s, *"hello");
936 assert!(s != *"world");
937 }
938
939 #[rstest]
940 fn test_try_from_bytes() {
941 let s: StackStr = b"hello".as_slice().try_into().unwrap();
942 assert_eq!(s.as_str(), "hello");
943 }
944}