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, non-null 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 null or invalid. This is safe to call from C
213    /// code for null and string-validation failures because it does not panic.
214    ///
215    /// # Safety
216    ///
217    /// - `ptr` must be null or 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        if ptr.is_null() {
221            return None;
222        }
223
224        // SAFETY: Caller guarantees ptr is valid and null-terminated
225        let cstr = unsafe { CStr::from_ptr(ptr) };
226        let s = cstr.to_str().ok()?;
227        Self::new_checked(s).ok()
228    }
229
230    /// Returns the string as a `&str`.
231    ///
232    /// This is an O(1) operation.
233    #[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        // SAFETY: We guarantee only valid ASCII is stored via check_valid_string_ascii
243        // on construction. ASCII is always valid UTF-8.
244        unsafe { std::str::from_utf8_unchecked(&self.value[..self.len as usize]) }
245    }
246
247    /// Returns the length in bytes (equal to character count for ASCII).
248    ///
249    /// This is an O(1) operation.
250    #[inline]
251    #[must_use]
252    pub const fn len(&self) -> usize {
253        self.len as usize
254    }
255
256    /// Returns `true` if the string is empty.
257    #[inline]
258    #[must_use]
259    pub const fn is_empty(&self) -> bool {
260        self.len == 0
261    }
262
263    /// Returns a pointer to the null-terminated C string.
264    #[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    /// Returns the value as a C string slice.
271    #[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        // SAFETY: We guarantee the string is null-terminated (buffer initialized to 0,
286        // and we only write up to len bytes leaving the null terminator intact),
287        // and no interior NUL bytes (rejected during construction).
288        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        // Only hash actual content, not padding
306        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    /// Creates an empty [`StackStr`] with length 0.
367    ///
368    /// Note: While [`StackStr::new`] rejects empty strings, `default()` creates
369    /// an empty placeholder. Use [`is_empty`](StackStr::is_empty) to check for this state.
370    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}"); // emoji
454    }
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; // Copy, not move
623        assert_eq!(a, b); // Both are still valid
624    }
625
626    #[rstest]
627    #[case("BINANCE")]
628    #[case("ETH-PERP")]
629    #[case("O-20231215-001")]
630    #[case("123456789012345678901234567890123456")] // 36 chars (max)
631    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        // SAFETY: StackStr buffer reserves at least 5 bytes (4 chars + null)
684        let p = unsafe { ptr.add(4) };
685        // SAFETY: position 4 is in-bounds and contains the null terminator
686        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]); // Non-ASCII bytes
706        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        // Tab is whitespace but valid ASCII
795        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        // ASCII: 'A' (65) < 'a' (97)
812        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}