nautilus_core/
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::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(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    pub fn new_checked(s: &str) -> anyhow::Result<Self> {
112        if s.is_empty() {
113            anyhow::bail!("String is empty");
114        }
115
116        if s.len() > STACKSTR_CAPACITY {
117            anyhow::bail!(
118                "String exceeds maximum length of {} characters, was {}",
119                STACKSTR_CAPACITY,
120                s.len()
121            );
122        }
123
124        if !s.is_ascii() {
125            anyhow::bail!("String contains non-ASCII character");
126        }
127
128        let bytes = s.as_bytes();
129        if bytes.contains(&0) {
130            anyhow::bail!("String contains interior NUL byte");
131        }
132
133        if bytes.iter().all(|b| b.is_ascii_whitespace()) {
134            anyhow::bail!("String contains only whitespace");
135        }
136
137        let mut value = [0u8; STACKSTR_BUFFER_SIZE];
138        value[..s.len()].copy_from_slice(bytes);
139        // Null terminator is already set (array initialized to 0)
140
141        Ok(Self {
142            value,
143            len: s.len() as u8,
144        })
145    }
146
147    /// Creates a [`StackStr`] from a byte slice.
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if:
152    /// - `bytes` is empty or contains only whitespace.
153    /// - `bytes` contains non-ASCII characters or interior NUL bytes.
154    /// - `bytes` exceeds 36 bytes (excluding trailing null terminator).
155    pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
156        // Strip trailing null terminator if present
157        let bytes = if bytes.last() == Some(&0) {
158            &bytes[..bytes.len() - 1]
159        } else {
160            bytes
161        };
162
163        let s = std::str::from_utf8(bytes).map_err(|e| anyhow::anyhow!("Invalid UTF-8: {e}"))?;
164
165        Self::new_checked(s)
166    }
167
168    /// Creates a [`StackStr`] from a C string pointer.
169    ///
170    /// For untrusted input from C code, use [`from_c_ptr_checked`](Self::from_c_ptr_checked)
171    /// to avoid panics crossing FFI boundaries.
172    ///
173    /// # Safety
174    ///
175    /// - `ptr` must be a valid pointer to a null-terminated C string.
176    /// - The string must contain only valid ASCII (no interior NUL bytes).
177    /// - The string must not exceed 36 characters.
178    ///
179    /// Violating these requirements causes a panic. If this function is called
180    /// from C code, such a panic is undefined behavior.
181    #[must_use]
182    pub unsafe fn from_c_ptr(ptr: *const c_char) -> Self {
183        // SAFETY: Caller guarantees ptr is valid and null-terminated
184        let cstr = unsafe { CStr::from_ptr(ptr) };
185        let s = cstr.to_str().expect("Invalid UTF-8 in C string");
186        Self::new(s)
187    }
188
189    /// Creates a [`StackStr`] from a C string pointer with validation.
190    ///
191    /// Returns `None` if the string is invalid. This is safe to call from C code
192    /// as it never panics on invalid input.
193    ///
194    /// # Safety
195    ///
196    /// - `ptr` must be a valid pointer to a null-terminated C string.
197    #[must_use]
198    pub unsafe fn from_c_ptr_checked(ptr: *const c_char) -> Option<Self> {
199        // SAFETY: Caller guarantees ptr is valid and null-terminated
200        let cstr = unsafe { CStr::from_ptr(ptr) };
201        let s = cstr.to_str().ok()?;
202        Self::new_checked(s).ok()
203    }
204
205    /// Returns the string as a `&str`.
206    ///
207    /// This is an O(1) operation.
208    #[inline]
209    #[must_use]
210    pub fn as_str(&self) -> &str {
211        debug_assert!(
212            self.len as usize <= STACKSTR_CAPACITY,
213            "StackStr len {} exceeds capacity {}",
214            self.len,
215            STACKSTR_CAPACITY
216        );
217        // SAFETY: We guarantee only valid ASCII is stored via check_valid_string_ascii
218        // on construction. ASCII is always valid UTF-8.
219        unsafe { std::str::from_utf8_unchecked(&self.value[..self.len as usize]) }
220    }
221
222    /// Returns the length in bytes (equal to character count for ASCII).
223    ///
224    /// This is an O(1) operation.
225    #[inline]
226    #[must_use]
227    pub const fn len(&self) -> usize {
228        self.len as usize
229    }
230
231    /// Returns `true` if the string is empty.
232    #[inline]
233    #[must_use]
234    pub const fn is_empty(&self) -> bool {
235        self.len == 0
236    }
237
238    /// Returns a pointer to the null-terminated C string.
239    #[inline]
240    #[must_use]
241    pub const fn as_ptr(&self) -> *const c_char {
242        self.value.as_ptr() as *const c_char
243    }
244
245    /// Returns the value as a C string slice.
246    #[inline]
247    #[must_use]
248    pub fn as_cstr(&self) -> &CStr {
249        debug_assert!(
250            self.len as usize <= STACKSTR_CAPACITY,
251            "StackStr len {} exceeds capacity {}",
252            self.len,
253            STACKSTR_CAPACITY
254        );
255        debug_assert!(
256            self.value[self.len as usize] == 0,
257            "StackStr missing null terminator at position {}",
258            self.len
259        );
260        // SAFETY: We guarantee the string is null-terminated (buffer initialized to 0,
261        // and we only write up to len bytes leaving the null terminator intact),
262        // and no interior NUL bytes (rejected during construction).
263        unsafe { CStr::from_bytes_with_nul_unchecked(&self.value[..=self.len as usize]) }
264    }
265}
266
267impl PartialEq for StackStr {
268    #[inline]
269    fn eq(&self, other: &Self) -> bool {
270        self.len == other.len
271            && self.value[..self.len as usize] == other.value[..other.len as usize]
272    }
273}
274
275impl Eq for StackStr {}
276
277impl Hash for StackStr {
278    #[inline]
279    fn hash<H: Hasher>(&self, state: &mut H) {
280        // Only hash actual content, not padding
281        self.value[..self.len as usize].hash(state);
282    }
283}
284
285impl Ord for StackStr {
286    fn cmp(&self, other: &Self) -> Ordering {
287        self.as_str().cmp(other.as_str())
288    }
289}
290
291impl PartialOrd for StackStr {
292    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
293        Some(self.cmp(other))
294    }
295}
296
297impl Display for StackStr {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        f.write_str(self.as_str())
300    }
301}
302
303impl Debug for StackStr {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        write!(f, "{:?}", self.as_str())
306    }
307}
308
309impl Serialize for StackStr {
310    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
311        serializer.serialize_str(self.as_str())
312    }
313}
314
315impl<'de> Deserialize<'de> for StackStr {
316    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
317        let s = <&str>::deserialize(deserializer)?;
318        Self::new_checked(s).map_err(serde::de::Error::custom)
319    }
320}
321
322impl From<&str> for StackStr {
323    fn from(s: &str) -> Self {
324        Self::new(s)
325    }
326}
327
328impl AsRef<str> for StackStr {
329    fn as_ref(&self) -> &str {
330        self.as_str()
331    }
332}
333
334impl Borrow<str> for StackStr {
335    fn borrow(&self) -> &str {
336        self.as_str()
337    }
338}
339
340impl Default for StackStr {
341    /// Creates an empty [`StackStr`] with length 0.
342    ///
343    /// Note: While [`StackStr::new`] rejects empty strings, `default()` creates
344    /// an empty placeholder. Use [`is_empty`](StackStr::is_empty) to check for this state.
345    fn default() -> Self {
346        Self {
347            value: [0u8; STACKSTR_BUFFER_SIZE],
348            len: 0,
349        }
350    }
351}
352
353impl Deref for StackStr {
354    type Target = str;
355
356    fn deref(&self) -> &Self::Target {
357        self.as_str()
358    }
359}
360
361impl PartialEq<&str> for StackStr {
362    fn eq(&self, other: &&str) -> bool {
363        self.as_str() == *other
364    }
365}
366
367impl PartialEq<str> for StackStr {
368    fn eq(&self, other: &str) -> bool {
369        self.as_str() == other
370    }
371}
372
373impl TryFrom<&[u8]> for StackStr {
374    type Error = anyhow::Error;
375
376    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
377        Self::from_bytes(bytes)
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use std::hash::{DefaultHasher, Hasher};
384
385    use ahash::AHashMap;
386    use rstest::rstest;
387
388    use super::*;
389
390    #[rstest]
391    fn test_new_valid() {
392        let s = StackStr::new("hello");
393        assert_eq!(s.as_str(), "hello");
394        assert_eq!(s.len(), 5);
395        assert!(!s.is_empty());
396    }
397
398    #[rstest]
399    fn test_max_length() {
400        let input = "x".repeat(36);
401        let s = StackStr::new(&input);
402        assert_eq!(s.len(), 36);
403        assert_eq!(s.as_str(), input);
404    }
405
406    #[rstest]
407    #[should_panic]
408    fn test_exceeds_max_length() {
409        let input = "x".repeat(37);
410        let _ = StackStr::new(&input);
411    }
412
413    #[rstest]
414    #[should_panic]
415    fn test_empty_string() {
416        let _ = StackStr::new("");
417    }
418
419    #[rstest]
420    #[should_panic]
421    fn test_whitespace_only() {
422        let _ = StackStr::new("   ");
423    }
424
425    #[rstest]
426    #[should_panic]
427    fn test_non_ascii() {
428        let _ = StackStr::new("hello\u{1F600}"); // emoji
429    }
430
431    #[rstest]
432    #[should_panic]
433    fn test_interior_nul_byte() {
434        let _ = StackStr::new("abc\0def");
435    }
436
437    #[rstest]
438    fn test_interior_nul_byte_checked() {
439        let result = StackStr::new_checked("abc\0def");
440        assert!(result.is_err());
441        assert!(result.unwrap_err().to_string().contains("NUL"));
442    }
443
444    #[rstest]
445    fn test_from_c_ptr_checked_valid() {
446        let cstring = std::ffi::CString::new("hello").unwrap();
447        let s = unsafe { StackStr::from_c_ptr_checked(cstring.as_ptr()) };
448        assert!(s.is_some());
449        assert_eq!(s.unwrap().as_str(), "hello");
450    }
451
452    #[rstest]
453    fn test_from_c_ptr_checked_too_long() {
454        let long = "x".repeat(37);
455        let cstring = std::ffi::CString::new(long).unwrap();
456        let s = unsafe { StackStr::from_c_ptr_checked(cstring.as_ptr()) };
457        assert!(s.is_none());
458    }
459
460    #[rstest]
461    fn test_equality() {
462        let a = StackStr::new("test");
463        let b = StackStr::new("test");
464        let c = StackStr::new("other");
465        assert_eq!(a, b);
466        assert_ne!(a, c);
467    }
468
469    #[rstest]
470    fn test_hash_consistency() {
471        use std::hash::DefaultHasher;
472
473        let a = StackStr::new("test");
474        let b = StackStr::new("test");
475
476        let hash_a = {
477            let mut h = DefaultHasher::new();
478            a.hash(&mut h);
479            h.finish()
480        };
481        let hash_b = {
482            let mut h = DefaultHasher::new();
483            b.hash(&mut h);
484            h.finish()
485        };
486
487        assert_eq!(hash_a, hash_b);
488    }
489
490    #[rstest]
491    fn test_hashmap_usage() {
492        let mut map = AHashMap::new();
493        map.insert(StackStr::new("key1"), 1);
494        map.insert(StackStr::new("key2"), 2);
495
496        assert_eq!(map.get(&StackStr::new("key1")), Some(&1));
497        assert_eq!(map.get(&StackStr::new("key2")), Some(&2));
498        assert_eq!(map.get(&StackStr::new("key3")), None);
499    }
500
501    #[rstest]
502    fn test_ordering() {
503        let a = StackStr::new("aaa");
504        let b = StackStr::new("bbb");
505        assert!(a < b);
506        assert!(b > a);
507    }
508
509    #[rstest]
510    fn test_c_compatibility() {
511        let s = StackStr::new("test");
512        let cstr = s.as_cstr();
513        assert_eq!(cstr.to_str().unwrap(), "test");
514    }
515
516    #[rstest]
517    fn test_as_ptr() {
518        let s = StackStr::new("test");
519        let ptr = s.as_ptr();
520        assert!(!ptr.is_null());
521
522        let cstr = unsafe { CStr::from_ptr(ptr) };
523        assert_eq!(cstr.to_str().unwrap(), "test");
524    }
525
526    #[rstest]
527    fn test_from_bytes() {
528        let s = StackStr::from_bytes(b"hello").unwrap();
529        assert_eq!(s.as_str(), "hello");
530    }
531
532    #[rstest]
533    fn test_from_bytes_with_null() {
534        let s = StackStr::from_bytes(b"hello\0").unwrap();
535        assert_eq!(s.as_str(), "hello");
536    }
537
538    #[rstest]
539    fn test_serde_roundtrip() {
540        let original = StackStr::new("test123");
541        let json = serde_json::to_string(&original).unwrap();
542        assert_eq!(json, "\"test123\"");
543
544        let deserialized: StackStr = serde_json::from_str(&json).unwrap();
545        assert_eq!(original, deserialized);
546    }
547
548    #[rstest]
549    fn test_display() {
550        let s = StackStr::new("hello");
551        assert_eq!(format!("{s}"), "hello");
552    }
553
554    #[rstest]
555    fn test_debug() {
556        let s = StackStr::new("hello");
557        assert_eq!(format!("{s:?}"), "\"hello\"");
558    }
559
560    #[rstest]
561    fn test_from_str() {
562        let s: StackStr = "hello".into();
563        assert_eq!(s.as_str(), "hello");
564    }
565
566    #[rstest]
567    fn test_as_ref() {
568        let s = StackStr::new("hello");
569        let r: &str = s.as_ref();
570        assert_eq!(r, "hello");
571    }
572
573    #[rstest]
574    fn test_borrow() {
575        let s = StackStr::new("hello");
576        let b: &str = s.borrow();
577        assert_eq!(b, "hello");
578    }
579
580    #[rstest]
581    fn test_default() {
582        let s = StackStr::default();
583        assert!(s.is_empty());
584        assert_eq!(s.len(), 0);
585    }
586
587    #[rstest]
588    fn test_copy_semantics() {
589        let a = StackStr::new("test");
590        let b = a; // Copy, not move
591        assert_eq!(a, b); // Both are still valid
592    }
593
594    #[rstest]
595    #[case("BINANCE")]
596    #[case("ETH-PERP")]
597    #[case("O-20231215-001")]
598    #[case("123456789012345678901234567890123456")] // 36 chars (max)
599    fn test_valid_identifiers(#[case] s: &str) {
600        let stack_str = StackStr::new(s);
601        assert_eq!(stack_str.as_str(), s);
602    }
603
604    #[rstest]
605    fn test_single_char() {
606        let s = StackStr::new("x");
607        assert_eq!(s.len(), 1);
608        assert_eq!(s.as_str(), "x");
609    }
610
611    #[rstest]
612    fn test_length_35() {
613        let input = "x".repeat(35);
614        let s = StackStr::new(&input);
615        assert_eq!(s.len(), 35);
616    }
617
618    #[rstest]
619    fn test_length_36_exact() {
620        let input = "x".repeat(36);
621        let s = StackStr::new(&input);
622        assert_eq!(s.len(), 36);
623        assert_eq!(s.as_str(), input);
624    }
625
626    #[rstest]
627    fn test_length_37_rejected() {
628        let input = "x".repeat(37);
629        let result = StackStr::new_checked(&input);
630        assert!(result.is_err());
631        assert!(result.unwrap_err().to_string().contains("exceeds"));
632    }
633
634    #[rstest]
635    fn test_struct_size() {
636        assert_eq!(std::mem::size_of::<StackStr>(), 38);
637    }
638
639    #[rstest]
640    fn test_value_field_at_offset_zero() {
641        let s = StackStr::new("hello");
642        let struct_ptr = &s as *const StackStr as *const u8;
643        let first_byte = unsafe { *struct_ptr };
644        assert_eq!(first_byte, b'h');
645    }
646
647    #[rstest]
648    fn test_null_terminator_present() {
649        let s = StackStr::new("test");
650        let ptr = s.as_ptr();
651        // Read byte at position 4 (after "test")
652        let null_byte = unsafe { *ptr.offset(4) };
653        assert_eq!(null_byte, 0);
654    }
655
656    #[rstest]
657    fn test_from_bytes_empty() {
658        let result = StackStr::from_bytes(b"");
659        assert!(result.is_err());
660    }
661
662    #[rstest]
663    fn test_from_bytes_interior_nul() {
664        let result = StackStr::from_bytes(b"abc\0def");
665        assert!(result.is_err());
666        assert!(result.unwrap_err().to_string().contains("NUL"));
667    }
668
669    #[rstest]
670    fn test_from_bytes_non_ascii() {
671        let result = StackStr::from_bytes(&[0x80, 0x81]); // Non-ASCII bytes
672        assert!(result.is_err());
673    }
674
675    #[rstest]
676    fn test_from_bytes_too_long() {
677        let bytes = [b'x'; 55];
678        let result = StackStr::from_bytes(&bytes);
679        assert!(result.is_err());
680    }
681
682    #[rstest]
683    fn test_from_bytes_whitespace_only() {
684        let result = StackStr::from_bytes(b"   ");
685        assert!(result.is_err());
686    }
687
688    #[rstest]
689    fn test_hash_differs_for_different_content() {
690        let a = StackStr::new("abc");
691        let b = StackStr::new("xyz");
692
693        let hash_a = {
694            let mut h = DefaultHasher::new();
695            a.hash(&mut h);
696            h.finish()
697        };
698        let hash_b = {
699            let mut h = DefaultHasher::new();
700            b.hash(&mut h);
701            h.finish()
702        };
703
704        assert_ne!(hash_a, hash_b);
705    }
706
707    #[rstest]
708    fn test_hash_ignores_padding() {
709        let a = StackStr::new("test");
710        let b = StackStr::new("test");
711
712        let hash_a = {
713            let mut h = DefaultHasher::new();
714            a.hash(&mut h);
715            h.finish()
716        };
717        let hash_b = {
718            let mut h = DefaultHasher::new();
719            b.hash(&mut h);
720            h.finish()
721        };
722
723        assert_eq!(hash_a, hash_b);
724    }
725
726    #[rstest]
727    fn test_serde_deserialize_too_long() {
728        let long = format!("\"{}\"", "x".repeat(55));
729        let result: Result<StackStr, _> = serde_json::from_str(&long);
730        assert!(result.is_err());
731    }
732
733    #[rstest]
734    fn test_serde_deserialize_empty() {
735        let result: Result<StackStr, _> = serde_json::from_str("\"\"");
736        assert!(result.is_err());
737    }
738
739    #[rstest]
740    fn test_serde_deserialize_non_ascii() {
741        let result: Result<StackStr, _> = serde_json::from_str("\"hello\u{1F600}\"");
742        assert!(result.is_err());
743    }
744
745    #[rstest]
746    #[case("!@#$%^&*()")]
747    #[case("hello-world_123")]
748    #[case("a.b.c.d")]
749    #[case("key=value")]
750    #[case("path/to/file")]
751    #[case("[bracket]")]
752    #[case("{curly}")]
753    fn test_special_ascii_chars(#[case] s: &str) {
754        let stack_str = StackStr::new(s);
755        assert_eq!(stack_str.as_str(), s);
756    }
757
758    #[rstest]
759    fn test_ascii_control_chars_tab() {
760        // Tab is whitespace but valid ASCII
761        let result = StackStr::new_checked("a\tb");
762        assert!(result.is_ok());
763        assert_eq!(result.unwrap().as_str(), "a\tb");
764    }
765
766    #[rstest]
767    fn test_ordering_same_prefix_different_length() {
768        let short = StackStr::new("abc");
769        let long = StackStr::new("abcd");
770        assert!(short < long);
771    }
772
773    #[rstest]
774    fn test_ordering_case_sensitive() {
775        let upper = StackStr::new("ABC");
776        let lower = StackStr::new("abc");
777        // ASCII: 'A' (65) < 'a' (97)
778        assert!(upper < lower);
779    }
780
781    #[rstest]
782    fn test_partial_cmp_returns_some() {
783        let a = StackStr::new("test");
784        let b = StackStr::new("test");
785        assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Equal));
786    }
787
788    #[rstest]
789    fn test_new_checked_error_empty() {
790        let err = StackStr::new_checked("").unwrap_err();
791        assert!(err.to_string().contains("empty"));
792    }
793
794    #[rstest]
795    fn test_new_checked_error_whitespace() {
796        let err = StackStr::new_checked("   ").unwrap_err();
797        assert!(err.to_string().contains("whitespace"));
798    }
799
800    #[rstest]
801    fn test_new_checked_error_too_long() {
802        let err = StackStr::new_checked(&"x".repeat(55)).unwrap_err();
803        assert!(err.to_string().contains("exceeds"));
804    }
805
806    #[rstest]
807    fn test_new_checked_error_non_ascii() {
808        let err = StackStr::new_checked("hello\u{1F600}").unwrap_err();
809        assert!(err.to_string().contains("non-ASCII"));
810    }
811
812    #[rstest]
813    fn test_new_checked_error_interior_nul() {
814        let err = StackStr::new_checked("abc\0def").unwrap_err();
815        assert!(err.to_string().contains("NUL"));
816    }
817
818    #[rstest]
819    fn test_clone_equals_original() {
820        let a = StackStr::new("test");
821        #[allow(clippy::clone_on_copy)]
822        let b = a.clone();
823        assert_eq!(a, b);
824    }
825
826    #[rstest]
827    fn test_deref() {
828        let s = StackStr::new("hello");
829        assert!(s.starts_with("hell"));
830        assert_eq!(s.len(), 5);
831    }
832
833    #[rstest]
834    fn test_partial_eq_str_literal() {
835        let s = StackStr::new("hello");
836        assert!(s == "hello");
837        assert!(s != "world");
838    }
839
840    #[rstest]
841    fn test_try_from_bytes() {
842        let s: StackStr = b"hello".as_slice().try_into().unwrap();
843        assert_eq!(s.as_str(), "hello");
844    }
845}