Skip to main content

nautilus_core/string/
stack_str.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A stack-allocated ASCII string type for efficient identifier storage.
17//!
18//! This module provides [`StackStr`], a fixed-capacity string type optimized for
19//! short identifier strings. Designed for use cases where:
20//!
21//! - Strings are known to be short (≤36 characters).
22//! - Stack allocation is preferred over heap allocation.
23//! - `Copy` semantics are beneficial.
24//! - C FFI compatibility is required.
25//!
26//! # ASCII requirement
27//!
28//! `StackStr` only accepts ASCII strings. This guarantees that 1 character == 1 byte,
29//! ensuring the buffer always holds exactly the capacity in characters. This aligns
30//! with identifier conventions which are inherently ASCII.
31//!
32//! | Property              | ASCII    | UTF-8               |
33//! |-----------------------|----------|---------------------|
34//! | Bytes per char        | Always 1 | 1-4                 |
35//! | 36 bytes holds        | 36 chars | 9-36 chars          |
36//! | Slice at any byte     | Safe     | May split codepoint |
37//! | `len()` == char count | Yes      | No                  |
38
39// Required for C FFI pointer handling and unchecked UTF-8/CStr conversions
40#![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
55/// Maximum capacity in characters for a [`StackStr`].
56pub const STACKSTR_CAPACITY: usize = 36;
57
58/// Fixed buffer size including null terminator (capacity + 1).
59const STACKSTR_BUFFER_SIZE: usize = STACKSTR_CAPACITY + 1;
60
61/// A stack-allocated ASCII string with a maximum capacity of 36 characters.
62///
63/// Optimized for short identifier strings with:
64/// - Stack allocation (no heap).
65/// - `Copy` semantics.
66/// - O(1) length access.
67/// - C FFI compatibility (null-terminated).
68///
69/// ASCII is required to guarantee 1 character == 1 byte, ensuring the buffer
70/// always holds exactly the capacity in characters. This aligns with identifier
71/// conventions which are inherently ASCII.
72///
73/// # Memory Layout
74///
75/// The `value` field is placed first so the struct pointer equals the string
76/// pointer, making C FFI more natural: `(char*)&stack_str` works directly.
77#[derive(Clone, Copy)]
78#[repr(C)]
79pub struct StackStr {
80    /// ASCII data with null terminator for C FFI.
81    value: [u8; 37], // STACKSTR_CAPACITY + 1
82    /// Length of the string in bytes (0-36).
83    len: u8,
84}
85
86impl StackStr {
87    /// Maximum length in characters.
88    pub const MAX_LEN: usize = STACKSTR_CAPACITY;
89
90    /// Creates a new [`StackStr`] from a string slice.
91    ///
92    /// # Panics
93    ///
94    /// Panics if:
95    /// - `s` is empty or contains only whitespace.
96    /// - `s` contains non-ASCII characters or interior NUL bytes.
97    /// - `s` exceeds 36 characters.
98    #[must_use]
99    pub fn new(s: &str) -> Self {
100        Self::new_checked(s).expect_display(FAILED)
101    }
102
103    /// Creates a new [`StackStr`] with validation, returning an error on failure.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if:
108    /// - `s` is empty or contains only whitespace.
109    /// - `s` contains non-ASCII characters or interior NUL bytes.
110    /// - `s` exceeds 36 characters.
111    #[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        // Null terminator is already set (array initialized to 0)
154
155        Ok(Self {
156            value,
157            len: s.len() as u8,
158        })
159    }
160
161    /// Creates a [`StackStr`] from a byte slice.
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if:
166    /// - `bytes` is empty or contains only whitespace.
167    /// - `bytes` contains non-ASCII characters or interior NUL bytes.
168    /// - `bytes` exceeds 36 bytes (excluding trailing null terminator).
169    pub fn from_bytes(bytes: &[u8]) -> CorrectnessResult<Self> {
170        // Strip trailing null terminator if present
171        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    /// Creates a [`StackStr`] from a C string pointer.
185    ///
186    /// For untrusted input from C code, use [`from_c_ptr_checked`](Self::from_c_ptr_checked)
187    /// to avoid panics crossing FFI boundaries.
188    ///
189    /// # Safety
190    ///
191    /// - `ptr` must be a valid pointer to a null-terminated C string.
192    /// - The string must contain only valid ASCII (no interior NUL bytes).
193    /// - The string must not exceed 36 characters.
194    ///
195    /// Violating these requirements causes a panic. If this function is called
196    /// from C code, such a panic is undefined behavior.
197    ///
198    /// # Panics
199    ///
200    /// Panics if the C string contains invalid UTF-8 or violates any of the
201    /// safety invariants listed above.
202    #[must_use]
203    pub unsafe fn from_c_ptr(ptr: *const c_char) -> Self {
204        // SAFETY: Caller guarantees ptr is valid and null-terminated
205        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    /// Creates a [`StackStr`] from a C string pointer with validation.
211    ///
212    /// Returns `None` if the string is invalid. This is safe to call from C code
213    /// as it never panics on invalid input.
214    ///
215    /// # Safety
216    ///
217    /// - `ptr` must be a valid pointer to a null-terminated C string.
218    #[must_use]
219    pub unsafe fn from_c_ptr_checked(ptr: *const c_char) -> Option<Self> {
220        // SAFETY: Caller guarantees ptr is valid and null-terminated
221        let cstr = unsafe { CStr::from_ptr(ptr) };
222        let s = cstr.to_str().ok()?;
223        Self::new_checked(s).ok()
224    }
225
226    /// Returns the string as a `&str`.
227    ///
228    /// This is an O(1) operation.
229    #[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        // SAFETY: We guarantee only valid ASCII is stored via check_valid_string_ascii
239        // on construction. ASCII is always valid UTF-8.
240        unsafe { std::str::from_utf8_unchecked(&self.value[..self.len as usize]) }
241    }
242
243    /// Returns the length in bytes (equal to character count for ASCII).
244    ///
245    /// This is an O(1) operation.
246    #[inline]
247    #[must_use]
248    pub const fn len(&self) -> usize {
249        self.len as usize
250    }
251
252    /// Returns `true` if the string is empty.
253    #[inline]
254    #[must_use]
255    pub const fn is_empty(&self) -> bool {
256        self.len == 0
257    }
258
259    /// Returns a pointer to the null-terminated C string.
260    #[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    /// Returns the value as a C string slice.
267    #[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        // SAFETY: We guarantee the string is null-terminated (buffer initialized to 0,
282        // and we only write up to len bytes leaving the null terminator intact),
283        // and no interior NUL bytes (rejected during construction).
284        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        // Only hash actual content, not padding
302        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    /// Creates an empty [`StackStr`] with length 0.
363    ///
364    /// Note: While [`StackStr::new`] rejects empty strings, `default()` creates
365    /// an empty placeholder. Use [`is_empty`](StackStr::is_empty) to check for this state.
366    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}"); // emoji
450    }
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; // Copy, not move
612        assert_eq!(a, b); // Both are still valid
613    }
614
615    #[rstest]
616    #[case("BINANCE")]
617    #[case("ETH-PERP")]
618    #[case("O-20231215-001")]
619    #[case("123456789012345678901234567890123456")] // 36 chars (max)
620    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        // Read byte at position 4 (after "test")
673        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]); // Non-ASCII bytes
693        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        // Tab is whitespace but valid ASCII
782        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        // ASCII: 'A' (65) < 'a' (97)
799        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}