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 let cstr = unsafe { CStr::from_ptr(ptr) };
222 let s = cstr.to_str().ok()?;
223 Self::new_checked(s).ok()
224 }
225
226 #[inline]
230 #[must_use]
231 pub fn as_str(&self) -> &str {
232 debug_assert!(
233 self.len as usize <= STACKSTR_CAPACITY,
234 "StackStr len {} exceeds capacity {}",
235 self.len,
236 STACKSTR_CAPACITY
237 );
238 unsafe { std::str::from_utf8_unchecked(&self.value[..self.len as usize]) }
241 }
242
243 #[inline]
247 #[must_use]
248 pub const fn len(&self) -> usize {
249 self.len as usize
250 }
251
252 #[inline]
254 #[must_use]
255 pub const fn is_empty(&self) -> bool {
256 self.len == 0
257 }
258
259 #[inline]
261 #[must_use]
262 pub const fn as_ptr(&self) -> *const c_char {
263 self.value.as_ptr().cast::<c_char>()
264 }
265
266 #[inline]
268 #[must_use]
269 pub fn as_cstr(&self) -> &CStr {
270 debug_assert!(
271 self.len as usize <= STACKSTR_CAPACITY,
272 "StackStr len {} exceeds capacity {}",
273 self.len,
274 STACKSTR_CAPACITY
275 );
276 debug_assert!(
277 self.value[self.len as usize] == 0,
278 "StackStr missing null terminator at position {}",
279 self.len
280 );
281 unsafe { CStr::from_bytes_with_nul_unchecked(&self.value[..=self.len as usize]) }
285 }
286}
287
288impl PartialEq for StackStr {
289 #[inline]
290 fn eq(&self, other: &Self) -> bool {
291 self.len == other.len
292 && self.value[..self.len as usize] == other.value[..other.len as usize]
293 }
294}
295
296impl Eq for StackStr {}
297
298impl Hash for StackStr {
299 #[inline]
300 fn hash<H: Hasher>(&self, state: &mut H) {
301 self.value[..self.len as usize].hash(state);
303 }
304}
305
306impl Ord for StackStr {
307 fn cmp(&self, other: &Self) -> Ordering {
308 self.as_str().cmp(other.as_str())
309 }
310}
311
312impl PartialOrd for StackStr {
313 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
314 Some(self.cmp(other))
315 }
316}
317
318impl Display for StackStr {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 f.write_str(self.as_str())
321 }
322}
323
324impl Debug for StackStr {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 write!(f, "{:?}", self.as_str())
327 }
328}
329
330impl Serialize for StackStr {
331 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
332 serializer.serialize_str(self.as_str())
333 }
334}
335
336impl<'de> Deserialize<'de> for StackStr {
337 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
338 let s = <&str>::deserialize(deserializer)?;
339 Self::new_checked(s).map_err(serde::de::Error::custom)
340 }
341}
342
343impl From<&str> for StackStr {
344 fn from(s: &str) -> Self {
345 Self::new(s)
346 }
347}
348
349impl AsRef<str> for StackStr {
350 fn as_ref(&self) -> &str {
351 self.as_str()
352 }
353}
354
355impl Borrow<str> for StackStr {
356 fn borrow(&self) -> &str {
357 self.as_str()
358 }
359}
360
361impl Default for StackStr {
362 fn default() -> Self {
367 Self {
368 value: [0u8; STACKSTR_BUFFER_SIZE],
369 len: 0,
370 }
371 }
372}
373
374impl Deref for StackStr {
375 type Target = str;
376
377 fn deref(&self) -> &Self::Target {
378 self.as_str()
379 }
380}
381
382impl PartialEq<&str> for StackStr {
383 fn eq(&self, other: &&str) -> bool {
384 self.as_str() == *other
385 }
386}
387
388impl PartialEq<str> for StackStr {
389 fn eq(&self, other: &str) -> bool {
390 self.as_str() == other
391 }
392}
393
394impl TryFrom<&[u8]> for StackStr {
395 type Error = CorrectnessError;
396
397 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
398 Self::from_bytes(bytes)
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use std::hash::{DefaultHasher, Hasher};
405
406 use ahash::AHashMap;
407 use rstest::rstest;
408
409 use super::*;
410
411 #[rstest]
412 fn test_new_valid() {
413 let s = StackStr::new("hello");
414 assert_eq!(s.as_str(), "hello");
415 assert_eq!(s.len(), 5);
416 assert!(!s.is_empty());
417 }
418
419 #[rstest]
420 fn test_max_length() {
421 let input = "x".repeat(36);
422 let s = StackStr::new(&input);
423 assert_eq!(s.len(), 36);
424 assert_eq!(s.as_str(), input);
425 }
426
427 #[rstest]
428 #[should_panic(expected = "Condition failed")]
429 fn test_exceeds_max_length() {
430 let input = "x".repeat(37);
431 let _ = StackStr::new(&input);
432 }
433
434 #[rstest]
435 #[should_panic(expected = "Condition failed")]
436 fn test_empty_string() {
437 let _ = StackStr::new("");
438 }
439
440 #[rstest]
441 #[should_panic(expected = "Condition failed")]
442 fn test_whitespace_only() {
443 let _ = StackStr::new(" ");
444 }
445
446 #[rstest]
447 #[should_panic(expected = "Condition failed")]
448 fn test_non_ascii() {
449 let _ = StackStr::new("hello\u{1F600}"); }
451
452 #[rstest]
453 #[should_panic(expected = "Condition failed")]
454 fn test_interior_nul_byte() {
455 let _ = StackStr::new("abc\0def");
456 }
457
458 #[rstest]
459 fn test_interior_nul_byte_checked() {
460 let result = StackStr::new_checked("abc\0def");
461 assert!(result.is_err());
462 assert!(result.unwrap_err().to_string().contains("NUL"));
463 }
464
465 #[rstest]
466 fn test_from_c_ptr_checked_valid() {
467 let cstring = std::ffi::CString::new("hello").unwrap();
468 let s = unsafe { StackStr::from_c_ptr_checked(cstring.as_ptr()) };
469 assert!(s.is_some());
470 assert_eq!(s.unwrap().as_str(), "hello");
471 }
472
473 #[rstest]
474 fn test_from_c_ptr_checked_too_long() {
475 let long = "x".repeat(37);
476 let cstring = std::ffi::CString::new(long).unwrap();
477 let s = unsafe { StackStr::from_c_ptr_checked(cstring.as_ptr()) };
478 assert!(s.is_none());
479 }
480
481 #[rstest]
482 fn test_equality() {
483 let a = StackStr::new("test");
484 let b = StackStr::new("test");
485 let c = StackStr::new("other");
486 assert_eq!(a, b);
487 assert_ne!(a, c);
488 }
489
490 #[rstest]
491 fn test_hash_consistency() {
492 use std::hash::DefaultHasher;
493
494 let a = StackStr::new("test");
495 let b = StackStr::new("test");
496
497 let hash_a = {
498 let mut h = DefaultHasher::new();
499 a.hash(&mut h);
500 h.finish()
501 };
502 let hash_b = {
503 let mut h = DefaultHasher::new();
504 b.hash(&mut h);
505 h.finish()
506 };
507
508 assert_eq!(hash_a, hash_b);
509 }
510
511 #[rstest]
512 fn test_hashmap_usage() {
513 let mut map = AHashMap::new();
514 map.insert(StackStr::new("key1"), 1);
515 map.insert(StackStr::new("key2"), 2);
516
517 assert_eq!(map.get(&StackStr::new("key1")), Some(&1));
518 assert_eq!(map.get(&StackStr::new("key2")), Some(&2));
519 assert_eq!(map.get(&StackStr::new("key3")), None);
520 }
521
522 #[rstest]
523 fn test_ordering() {
524 let a = StackStr::new("aaa");
525 let b = StackStr::new("bbb");
526 assert!(a < b);
527 assert!(b > a);
528 }
529
530 #[rstest]
531 fn test_c_compatibility() {
532 let s = StackStr::new("test");
533 let cstr = s.as_cstr();
534 assert_eq!(cstr.to_str().unwrap(), "test");
535 }
536
537 #[rstest]
538 fn test_as_ptr() {
539 let s = StackStr::new("test");
540 let ptr = s.as_ptr();
541 assert!(!ptr.is_null());
542
543 let cstr = unsafe { CStr::from_ptr(ptr) };
544 assert_eq!(cstr.to_str().unwrap(), "test");
545 }
546
547 #[rstest]
548 fn test_from_bytes() {
549 let s = StackStr::from_bytes(b"hello").unwrap();
550 assert_eq!(s.as_str(), "hello");
551 }
552
553 #[rstest]
554 fn test_from_bytes_with_null() {
555 let s = StackStr::from_bytes(b"hello\0").unwrap();
556 assert_eq!(s.as_str(), "hello");
557 }
558
559 #[rstest]
560 fn test_serde_roundtrip() {
561 let original = StackStr::new("test123");
562 let json = serde_json::to_string(&original).unwrap();
563 assert_eq!(json, "\"test123\"");
564
565 let deserialized: StackStr = serde_json::from_str(&json).unwrap();
566 assert_eq!(original, deserialized);
567 }
568
569 #[rstest]
570 fn test_display() {
571 let s = StackStr::new("hello");
572 assert_eq!(format!("{s}"), "hello");
573 }
574
575 #[rstest]
576 fn test_debug() {
577 let s = StackStr::new("hello");
578 assert_eq!(format!("{s:?}"), "\"hello\"");
579 }
580
581 #[rstest]
582 fn test_from_str() {
583 let s: StackStr = "hello".into();
584 assert_eq!(s.as_str(), "hello");
585 }
586
587 #[rstest]
588 fn test_as_ref() {
589 let s = StackStr::new("hello");
590 let r: &str = s.as_ref();
591 assert_eq!(r, "hello");
592 }
593
594 #[rstest]
595 fn test_borrow() {
596 let s = StackStr::new("hello");
597 let b: &str = s.borrow();
598 assert_eq!(b, "hello");
599 }
600
601 #[rstest]
602 fn test_default() {
603 let s = StackStr::default();
604 assert!(s.is_empty());
605 assert_eq!(s.len(), 0);
606 }
607
608 #[rstest]
609 fn test_copy_semantics() {
610 let a = StackStr::new("test");
611 let b = a; assert_eq!(a, b); }
614
615 #[rstest]
616 #[case("BINANCE")]
617 #[case("ETH-PERP")]
618 #[case("O-20231215-001")]
619 #[case("123456789012345678901234567890123456")] fn test_valid_identifiers(#[case] s: &str) {
621 let stack_str = StackStr::new(s);
622 assert_eq!(stack_str.as_str(), s);
623 }
624
625 #[rstest]
626 fn test_single_char() {
627 let s = StackStr::new("x");
628 assert_eq!(s.len(), 1);
629 assert_eq!(s.as_str(), "x");
630 }
631
632 #[rstest]
633 fn test_length_35() {
634 let input = "x".repeat(35);
635 let s = StackStr::new(&input);
636 assert_eq!(s.len(), 35);
637 }
638
639 #[rstest]
640 fn test_length_36_exact() {
641 let input = "x".repeat(36);
642 let s = StackStr::new(&input);
643 assert_eq!(s.len(), 36);
644 assert_eq!(s.as_str(), input);
645 }
646
647 #[rstest]
648 fn test_length_37_rejected() {
649 let input = "x".repeat(37);
650 let result = StackStr::new_checked(&input);
651 assert!(result.is_err());
652 assert!(result.unwrap_err().to_string().contains("exceeds"));
653 }
654
655 #[rstest]
656 fn test_struct_size() {
657 assert_eq!(std::mem::size_of::<StackStr>(), 38);
658 }
659
660 #[rstest]
661 fn test_value_field_at_offset_zero() {
662 let s = StackStr::new("hello");
663 let struct_ptr = std::ptr::from_ref(&s).cast::<u8>();
664 let first_byte = unsafe { *struct_ptr };
665 assert_eq!(first_byte, b'h');
666 }
667
668 #[rstest]
669 fn test_null_terminator_present() {
670 let s = StackStr::new("test");
671 let ptr = s.as_ptr();
672 let null_byte = unsafe { *ptr.add(4) };
674 assert_eq!(null_byte, 0);
675 }
676
677 #[rstest]
678 fn test_from_bytes_empty() {
679 let result = StackStr::from_bytes(b"");
680 assert!(result.is_err());
681 }
682
683 #[rstest]
684 fn test_from_bytes_interior_nul() {
685 let result = StackStr::from_bytes(b"abc\0def");
686 assert!(result.is_err());
687 assert!(result.unwrap_err().to_string().contains("NUL"));
688 }
689
690 #[rstest]
691 fn test_from_bytes_non_ascii() {
692 let result = StackStr::from_bytes(&[0x80, 0x81]); assert!(result.is_err());
694 }
695
696 #[rstest]
697 fn test_from_bytes_too_long() {
698 let bytes = [b'x'; 55];
699 let result = StackStr::from_bytes(&bytes);
700 assert!(result.is_err());
701 }
702
703 #[rstest]
704 fn test_from_bytes_whitespace_only() {
705 let result = StackStr::from_bytes(b" ");
706 assert!(result.is_err());
707 }
708
709 #[rstest]
710 fn test_hash_differs_for_different_content() {
711 let a = StackStr::new("abc");
712 let b = StackStr::new("xyz");
713
714 let hash_a = {
715 let mut h = DefaultHasher::new();
716 a.hash(&mut h);
717 h.finish()
718 };
719 let hash_b = {
720 let mut h = DefaultHasher::new();
721 b.hash(&mut h);
722 h.finish()
723 };
724
725 assert_ne!(hash_a, hash_b);
726 }
727
728 #[rstest]
729 fn test_hash_ignores_padding() {
730 let a = StackStr::new("test");
731 let b = StackStr::new("test");
732
733 let hash_a = {
734 let mut h = DefaultHasher::new();
735 a.hash(&mut h);
736 h.finish()
737 };
738 let hash_b = {
739 let mut h = DefaultHasher::new();
740 b.hash(&mut h);
741 h.finish()
742 };
743
744 assert_eq!(hash_a, hash_b);
745 }
746
747 #[rstest]
748 fn test_serde_deserialize_too_long() {
749 let long = format!("\"{}\"", "x".repeat(55));
750 let result: Result<StackStr, _> = serde_json::from_str(&long);
751 assert!(result.is_err());
752 }
753
754 #[rstest]
755 fn test_serde_deserialize_empty() {
756 let result: Result<StackStr, _> = serde_json::from_str("\"\"");
757 assert!(result.is_err());
758 }
759
760 #[rstest]
761 fn test_serde_deserialize_non_ascii() {
762 let result: Result<StackStr, _> = serde_json::from_str("\"hello\u{1F600}\"");
763 assert!(result.is_err());
764 }
765
766 #[rstest]
767 #[case("!@#$%^&*()")]
768 #[case("hello-world_123")]
769 #[case("a.b.c.d")]
770 #[case("key=value")]
771 #[case("path/to/file")]
772 #[case("[bracket]")]
773 #[case("{curly}")]
774 fn test_special_ascii_chars(#[case] s: &str) {
775 let stack_str = StackStr::new(s);
776 assert_eq!(stack_str.as_str(), s);
777 }
778
779 #[rstest]
780 fn test_ascii_control_chars_tab() {
781 let result = StackStr::new_checked("a\tb");
783 assert!(result.is_ok());
784 assert_eq!(result.unwrap().as_str(), "a\tb");
785 }
786
787 #[rstest]
788 fn test_ordering_same_prefix_different_length() {
789 let short = StackStr::new("abc");
790 let long = StackStr::new("abcd");
791 assert!(short < long);
792 }
793
794 #[rstest]
795 fn test_ordering_case_sensitive() {
796 let upper = StackStr::new("ABC");
797 let lower = StackStr::new("abc");
798 assert!(upper < lower);
800 }
801
802 #[rstest]
803 fn test_partial_cmp_returns_some() {
804 let a = StackStr::new("test");
805 let b = StackStr::new("test");
806 assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Equal));
807 }
808
809 #[rstest]
810 fn test_new_checked_error_empty() {
811 let err = StackStr::new_checked("").unwrap_err();
812 assert!(err.to_string().contains("empty"));
813 }
814
815 #[rstest]
816 fn test_new_checked_error_whitespace() {
817 let err = StackStr::new_checked(" ").unwrap_err();
818 assert!(err.to_string().contains("whitespace"));
819 }
820
821 #[rstest]
822 fn test_new_checked_error_too_long() {
823 let err = StackStr::new_checked(&"x".repeat(55)).unwrap_err();
824 assert!(err.to_string().contains("exceeds"));
825 }
826
827 #[rstest]
828 fn test_new_checked_error_non_ascii() {
829 let err = StackStr::new_checked("hello\u{1F600}").unwrap_err();
830 assert!(err.to_string().contains("non-ASCII"));
831 }
832
833 #[rstest]
834 fn test_new_checked_error_interior_nul() {
835 let err = StackStr::new_checked("abc\0def").unwrap_err();
836 assert!(err.to_string().contains("NUL"));
837 }
838
839 #[rstest]
840 fn test_clone_equals_original() {
841 let a = StackStr::new("test");
842 #[expect(clippy::clone_on_copy)]
843 let b = a.clone();
844 assert_eq!(a, b);
845 }
846
847 #[rstest]
848 fn test_deref() {
849 let s = StackStr::new("hello");
850 assert!(s.starts_with("hell"));
851 assert_eq!(s.len(), 5);
852 }
853
854 #[rstest]
855 fn test_partial_eq_str_literal() {
856 let s = StackStr::new("hello");
857 assert!(s == "hello");
858 assert!(s != "world");
859 }
860
861 #[rstest]
862 fn test_try_from_bytes() {
863 let s: StackStr = b"hello".as_slice().try_into().unwrap();
864 assert_eq!(s.as_str(), "hello");
865 }
866}