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