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(u8::is_ascii_whitespace) {
146            return Err(CorrectnessError::PredicateViolation {
147                message: "String contains only whitespace".to_string(),
148            });
149        }
150
151        let mut value = [0u8; STACKSTR_BUFFER_SIZE];
152        value[..s.len()].copy_from_slice(bytes);
153        // 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_from_c_ptr_valid() {
494        let cstring = std::ffi::CString::new("hello").unwrap();
495        let s = unsafe { StackStr::from_c_ptr(cstring.as_ptr()) };
496        assert_eq!(s.as_str(), "hello");
497    }
498
499    #[rstest]
500    #[should_panic(expected = "Invalid UTF-8 in C string")]
501    fn test_from_c_ptr_invalid_utf8_panics() {
502        let bytes = vec![0xFF];
503        let cstring = unsafe { std::ffi::CString::from_vec_unchecked(bytes) };
504        let _ = unsafe { StackStr::from_c_ptr(cstring.as_ptr()) };
505    }
506
507    #[rstest]
508    #[should_panic(
509        expected = "Condition failed: String exceeds maximum length of 36 characters, was 37"
510    )]
511    fn test_from_c_ptr_too_long_panics() {
512        let long = "x".repeat(37);
513        let cstring = std::ffi::CString::new(long).unwrap();
514        let _ = unsafe { StackStr::from_c_ptr(cstring.as_ptr()) };
515    }
516
517    #[rstest]
518    fn test_equality() {
519        let a = StackStr::new("test");
520        let b = StackStr::new("test");
521        let c = StackStr::new("other");
522        assert_eq!(a, b);
523        assert_ne!(a, c);
524    }
525
526    #[rstest]
527    fn test_inequality_same_length() {
528        let a = StackStr::new("test");
529        let b = StackStr::new("tent");
530        assert_ne!(a, b);
531    }
532
533    #[rstest]
534    fn test_hash_consistency() {
535        use std::hash::DefaultHasher;
536
537        let a = StackStr::new("test");
538        let b = StackStr::new("test");
539
540        let hash_a = {
541            let mut h = DefaultHasher::new();
542            a.hash(&mut h);
543            h.finish()
544        };
545        let hash_b = {
546            let mut h = DefaultHasher::new();
547            b.hash(&mut h);
548            h.finish()
549        };
550
551        assert_eq!(hash_a, hash_b);
552    }
553
554    #[rstest]
555    fn test_hashmap_usage() {
556        let mut map = AHashMap::new();
557        map.insert(StackStr::new("key1"), 1);
558        map.insert(StackStr::new("key2"), 2);
559
560        assert_eq!(map.get(&StackStr::new("key1")), Some(&1));
561        assert_eq!(map.get(&StackStr::new("key2")), Some(&2));
562        assert_eq!(map.get(&StackStr::new("key3")), None);
563    }
564
565    #[rstest]
566    fn test_ordering() {
567        let a = StackStr::new("aaa");
568        let b = StackStr::new("bbb");
569        assert!(a < b);
570        assert!(b > a);
571    }
572
573    #[rstest]
574    fn test_c_compatibility() {
575        let s = StackStr::new("test");
576        let cstr = s.as_cstr();
577        assert_eq!(cstr.to_str().unwrap(), "test");
578    }
579
580    #[rstest]
581    fn test_as_ptr() {
582        let s = StackStr::new("test");
583        let ptr = s.as_ptr();
584        assert!(!ptr.is_null());
585
586        let cstr = unsafe { CStr::from_ptr(ptr) };
587        assert_eq!(cstr.to_str().unwrap(), "test");
588    }
589
590    #[rstest]
591    fn test_from_bytes() {
592        let s = StackStr::from_bytes(b"hello").unwrap();
593        assert_eq!(s.as_str(), "hello");
594    }
595
596    #[rstest]
597    fn test_from_bytes_with_null() {
598        let s = StackStr::from_bytes(b"hello\0").unwrap();
599        assert_eq!(s.as_str(), "hello");
600    }
601
602    #[rstest]
603    fn test_serde_roundtrip() {
604        let original = StackStr::new("test123");
605        let json = serde_json::to_string(&original).unwrap();
606        assert_eq!(json, "\"test123\"");
607
608        let deserialized: StackStr = serde_json::from_str(&json).unwrap();
609        assert_eq!(original, deserialized);
610    }
611
612    #[rstest]
613    fn test_display() {
614        let s = StackStr::new("hello");
615        assert_eq!(format!("{s}"), "hello");
616    }
617
618    #[rstest]
619    fn test_debug() {
620        let s = StackStr::new("hello");
621        assert_eq!(format!("{s:?}"), "\"hello\"");
622    }
623
624    #[rstest]
625    fn test_from_str() {
626        let s: StackStr = "hello".into();
627        assert_eq!(s.as_str(), "hello");
628    }
629
630    #[rstest]
631    fn test_as_ref() {
632        let s = StackStr::new("hello");
633        let r: &str = s.as_ref();
634        assert_eq!(r, "hello");
635    }
636
637    #[rstest]
638    fn test_borrow() {
639        let s = StackStr::new("hello");
640        let b: &str = s.borrow();
641        assert_eq!(b, "hello");
642    }
643
644    #[rstest]
645    fn test_default() {
646        let s = StackStr::default();
647        assert!(s.is_empty());
648        assert_eq!(s.len(), 0);
649    }
650
651    #[rstest]
652    fn test_copy_semantics() {
653        let a = StackStr::new("test");
654        let b = a; // Copy, not move
655        assert_eq!(a, b); // Both are still valid
656    }
657
658    #[rstest]
659    #[case("BINANCE")]
660    #[case("ETH-PERP")]
661    #[case("O-20231215-001")]
662    #[case("123456789012345678901234567890123456")] // 36 chars (max)
663    fn test_valid_identifiers(#[case] s: &str) {
664        let stack_str = StackStr::new(s);
665        assert_eq!(stack_str.as_str(), s);
666    }
667
668    #[rstest]
669    fn test_single_char() {
670        let s = StackStr::new("x");
671        assert_eq!(s.len(), 1);
672        assert_eq!(s.as_str(), "x");
673    }
674
675    #[rstest]
676    fn test_length_35() {
677        let input = "x".repeat(35);
678        let s = StackStr::new(&input);
679        assert_eq!(s.len(), 35);
680    }
681
682    #[rstest]
683    fn test_length_36_exact() {
684        let input = "x".repeat(36);
685        let s = StackStr::new(&input);
686        assert_eq!(s.len(), 36);
687        assert_eq!(s.as_str(), input);
688    }
689
690    #[rstest]
691    fn test_length_37_rejected() {
692        let input = "x".repeat(37);
693        let result = StackStr::new_checked(&input);
694        assert!(result.is_err());
695        assert!(result.unwrap_err().to_string().contains("exceeds"));
696    }
697
698    #[rstest]
699    fn test_struct_size() {
700        assert_eq!(std::mem::size_of::<StackStr>(), 38);
701    }
702
703    #[rstest]
704    fn test_value_field_at_offset_zero() {
705        let s = StackStr::new("hello");
706        let struct_ptr = std::ptr::from_ref(&s).cast::<u8>();
707        let first_byte = unsafe { *struct_ptr };
708        assert_eq!(first_byte, b'h');
709    }
710
711    #[rstest]
712    fn test_null_terminator_present() {
713        let s = StackStr::new("test");
714        let ptr = s.as_ptr();
715        // SAFETY: StackStr buffer reserves at least 5 bytes (4 chars + null)
716        let p = unsafe { ptr.add(4) };
717        // SAFETY: position 4 is in-bounds and contains the null terminator
718        let null_byte = unsafe { *p };
719        assert_eq!(null_byte, 0);
720    }
721
722    #[rstest]
723    fn test_from_bytes_empty() {
724        let result = StackStr::from_bytes(b"");
725        assert!(result.is_err());
726    }
727
728    #[rstest]
729    fn test_from_bytes_interior_nul() {
730        let result = StackStr::from_bytes(b"abc\0def");
731        assert!(result.is_err());
732        assert!(result.unwrap_err().to_string().contains("NUL"));
733    }
734
735    #[rstest]
736    fn test_from_bytes_non_ascii() {
737        let result = StackStr::from_bytes(&[0x80, 0x81]); // Non-ASCII bytes
738        assert!(result.is_err());
739    }
740
741    #[rstest]
742    fn test_from_bytes_too_long() {
743        let bytes = [b'x'; 55];
744        let result = StackStr::from_bytes(&bytes);
745        assert!(result.is_err());
746    }
747
748    #[rstest]
749    fn test_from_bytes_whitespace_only() {
750        let result = StackStr::from_bytes(b"   ");
751        assert!(result.is_err());
752    }
753
754    #[rstest]
755    fn test_hash_differs_for_different_content() {
756        let a = StackStr::new("abc");
757        let b = StackStr::new("xyz");
758
759        let hash_a = {
760            let mut h = DefaultHasher::new();
761            a.hash(&mut h);
762            h.finish()
763        };
764        let hash_b = {
765            let mut h = DefaultHasher::new();
766            b.hash(&mut h);
767            h.finish()
768        };
769
770        assert_ne!(hash_a, hash_b);
771    }
772
773    #[rstest]
774    fn test_hash_ignores_padding() {
775        let a = StackStr::new("test");
776        let b = StackStr::new("test");
777
778        let hash_a = {
779            let mut h = DefaultHasher::new();
780            a.hash(&mut h);
781            h.finish()
782        };
783        let hash_b = {
784            let mut h = DefaultHasher::new();
785            b.hash(&mut h);
786            h.finish()
787        };
788
789        assert_eq!(hash_a, hash_b);
790    }
791
792    #[rstest]
793    fn test_serde_deserialize_too_long() {
794        let long = format!("\"{}\"", "x".repeat(55));
795        let result: Result<StackStr, _> = serde_json::from_str(&long);
796        assert!(result.is_err());
797    }
798
799    #[rstest]
800    fn test_serde_deserialize_empty() {
801        let result: Result<StackStr, _> = serde_json::from_str("\"\"");
802        assert!(result.is_err());
803    }
804
805    #[rstest]
806    fn test_serde_deserialize_non_ascii() {
807        let result: Result<StackStr, _> = serde_json::from_str("\"hello\u{1F600}\"");
808        assert!(result.is_err());
809    }
810
811    #[rstest]
812    #[case("!@#$%^&*()")]
813    #[case("hello-world_123")]
814    #[case("a.b.c.d")]
815    #[case("key=value")]
816    #[case("path/to/file")]
817    #[case("[bracket]")]
818    #[case("{curly}")]
819    fn test_special_ascii_chars(#[case] s: &str) {
820        let stack_str = StackStr::new(s);
821        assert_eq!(stack_str.as_str(), s);
822    }
823
824    #[rstest]
825    fn test_ascii_control_chars_tab() {
826        // Tab is whitespace but valid ASCII
827        let result = StackStr::new_checked("a\tb");
828        assert!(result.is_ok());
829        assert_eq!(result.unwrap().as_str(), "a\tb");
830    }
831
832    #[rstest]
833    fn test_ordering_same_prefix_different_length() {
834        let short = StackStr::new("abc");
835        let long = StackStr::new("abcd");
836        assert!(short < long);
837    }
838
839    #[rstest]
840    fn test_ordering_case_sensitive() {
841        let upper = StackStr::new("ABC");
842        let lower = StackStr::new("abc");
843        // ASCII: 'A' (65) < 'a' (97)
844        assert!(upper < lower);
845    }
846
847    #[rstest]
848    fn test_partial_cmp_returns_some() {
849        let a = StackStr::new("test");
850        let b = StackStr::new("test");
851        assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Equal));
852    }
853
854    #[rstest]
855    fn test_new_checked_error_empty() {
856        let err = StackStr::new_checked("").unwrap_err();
857        assert!(err.to_string().contains("empty"));
858    }
859
860    #[rstest]
861    fn test_new_checked_error_whitespace() {
862        let err = StackStr::new_checked("   ").unwrap_err();
863        assert!(err.to_string().contains("whitespace"));
864    }
865
866    #[rstest]
867    fn test_new_checked_error_too_long() {
868        let err = StackStr::new_checked(&"x".repeat(55)).unwrap_err();
869        assert!(err.to_string().contains("exceeds"));
870    }
871
872    #[rstest]
873    fn test_new_checked_error_non_ascii() {
874        let err = StackStr::new_checked("hello\u{1F600}").unwrap_err();
875        assert!(err.to_string().contains("non-ASCII"));
876    }
877
878    #[rstest]
879    fn test_new_checked_error_interior_nul() {
880        let err = StackStr::new_checked("abc\0def").unwrap_err();
881        assert!(err.to_string().contains("NUL"));
882    }
883
884    #[rstest]
885    fn test_clone_equals_original() {
886        let a = StackStr::new("test");
887        #[expect(clippy::clone_on_copy)]
888        let b = a.clone();
889        assert_eq!(a, b);
890    }
891
892    #[rstest]
893    fn test_deref() {
894        let s = StackStr::new("hello");
895        assert!(s.starts_with("hell"));
896        assert_eq!(s.len(), 5);
897    }
898
899    #[rstest]
900    fn test_partial_eq_str_literal() {
901        let s = StackStr::new("hello");
902        assert_eq!(s, "hello");
903        assert!(s != "world");
904    }
905
906    #[rstest]
907    fn test_partial_eq_str_unsized() {
908        let s = StackStr::new("hello");
909        assert_eq!(s, *"hello");
910        assert!(s != *"world");
911    }
912
913    #[rstest]
914    fn test_try_from_bytes() {
915        let s: StackStr = b"hello".as_slice().try_into().unwrap();
916        assert_eq!(s.as_str(), "hello");
917    }
918}