facet_value/
string.rs

1//! String value type.
2
3#[cfg(feature = "alloc")]
4use alloc::alloc::{Layout, alloc, dealloc};
5#[cfg(feature = "alloc")]
6use alloc::string::String;
7use core::borrow::Borrow;
8use core::cmp::Ordering;
9use core::fmt::{self, Debug, Formatter};
10use core::hash::{Hash, Hasher};
11use core::mem;
12use core::ops::Deref;
13use core::ptr;
14use static_assertions::const_assert;
15use static_assertions::const_assert_eq;
16
17use crate::value::{TypeTag, Value};
18
19/// Flag indicating the string is marked as "safe" (e.g., pre-escaped HTML).
20/// This uses the high bit of the length field in StringHeader.
21const SAFE_FLAG: usize = 1usize << (usize::BITS - 1);
22
23/// Header for heap-allocated strings.
24#[repr(C, align(8))]
25struct StringHeader {
26    /// Length of the string in bytes.
27    /// The high bit may be set to indicate a "safe" string (see SAFE_FLAG).
28    len: usize,
29    // String data follows immediately after
30}
31
32impl StringHeader {
33    /// Returns the actual length of the string, masking out the safe flag.
34    #[inline]
35    fn actual_len(&self) -> usize {
36        self.len & !SAFE_FLAG
37    }
38
39    /// Returns true if the safe flag is set.
40    #[inline]
41    fn is_safe(&self) -> bool {
42        self.len & SAFE_FLAG != 0
43    }
44}
45
46/// A string value.
47///
48/// `VString` stores UTF-8 string data. Short strings (up to 7 bytes on 64-bit targets) are
49/// embedded directly in the `Value` pointer bits, while longer strings fall back to heap storage.
50#[repr(transparent)]
51#[derive(Clone)]
52pub struct VString(pub(crate) Value);
53
54impl VString {
55    const INLINE_WORD_BYTES: usize = mem::size_of::<usize>();
56    const INLINE_DATA_OFFSET: usize = 1;
57    const INLINE_CAP_BYTES: usize = Self::INLINE_WORD_BYTES - Self::INLINE_DATA_OFFSET;
58    pub(crate) const INLINE_LEN_MAX: usize = {
59        const LEN_MASK: usize = (1 << (8 - 3)) - 1;
60        let cap = mem::size_of::<usize>() - 1;
61        if cap < LEN_MASK { cap } else { LEN_MASK }
62    };
63    const INLINE_LEN_SHIFT: u8 = 3;
64
65    fn layout(len: usize) -> Layout {
66        Layout::new::<StringHeader>()
67            .extend(Layout::array::<u8>(len).unwrap())
68            .unwrap()
69            .0
70            .pad_to_align()
71    }
72
73    #[cfg(feature = "alloc")]
74    fn alloc(s: &str) -> *mut StringHeader {
75        unsafe {
76            let layout = Self::layout(s.len());
77            let ptr = alloc(layout).cast::<StringHeader>();
78            (*ptr).len = s.len();
79
80            // Copy string data
81            let data_ptr = ptr.add(1).cast::<u8>();
82            ptr::copy_nonoverlapping(s.as_ptr(), data_ptr, s.len());
83
84            ptr
85        }
86    }
87
88    #[cfg(feature = "alloc")]
89    fn dealloc_ptr(ptr: *mut StringHeader) {
90        unsafe {
91            let len = (*ptr).actual_len();
92            let layout = Self::layout(len);
93            dealloc(ptr.cast::<u8>(), layout);
94        }
95    }
96
97    fn header(&self) -> &StringHeader {
98        debug_assert!(!self.is_inline());
99        unsafe { &*(self.0.heap_ptr() as *const StringHeader) }
100    }
101
102    fn data_ptr(&self) -> *const u8 {
103        debug_assert!(!self.is_inline());
104        // Go through heap_ptr directly to avoid creating intermediate reference
105        // that would limit provenance to just the header
106        unsafe { (self.0.heap_ptr() as *const StringHeader).add(1).cast() }
107    }
108
109    /// Creates a new string from a `&str`.
110    #[cfg(feature = "alloc")]
111    #[must_use]
112    pub fn new(s: &str) -> Self {
113        if Self::can_inline(s.len()) {
114            return Self::new_inline(s);
115        }
116        unsafe {
117            let ptr = Self::alloc(s);
118            VString(Value::new_ptr(ptr.cast(), TypeTag::StringOrNull))
119        }
120    }
121
122    /// Creates an empty string.
123    #[cfg(feature = "alloc")]
124    #[must_use]
125    pub fn empty() -> Self {
126        Self::new_inline("")
127    }
128
129    /// Returns the length of the string in bytes.
130    #[must_use]
131    pub fn len(&self) -> usize {
132        if self.is_inline() {
133            self.inline_len()
134        } else {
135            self.header().actual_len()
136        }
137    }
138
139    /// Returns `true` if the string is empty.
140    #[must_use]
141    pub fn is_empty(&self) -> bool {
142        self.len() == 0
143    }
144
145    /// Returns the string as a `&str`.
146    #[must_use]
147    pub fn as_str(&self) -> &str {
148        unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
149    }
150
151    /// Returns the string as a byte slice.
152    #[must_use]
153    pub fn as_bytes(&self) -> &[u8] {
154        if self.is_inline() {
155            unsafe { core::slice::from_raw_parts(self.inline_data_ptr(), self.inline_len()) }
156        } else {
157            unsafe { core::slice::from_raw_parts(self.data_ptr(), self.len()) }
158        }
159    }
160
161    pub(crate) fn clone_impl(&self) -> Value {
162        if self.is_safe() {
163            // Preserve the safe flag through clone
164            VSafeString::new(self.as_str()).0
165        } else {
166            VString::new(self.as_str()).0
167        }
168    }
169
170    pub(crate) fn drop_impl(&mut self) {
171        if self.is_inline() {
172            return;
173        }
174        unsafe {
175            Self::dealloc_ptr(self.0.heap_ptr_mut().cast());
176        }
177    }
178
179    #[inline]
180    fn is_inline(&self) -> bool {
181        self.0.is_inline_string()
182    }
183
184    #[inline]
185    fn can_inline(len: usize) -> bool {
186        len <= Self::INLINE_LEN_MAX && len <= Self::INLINE_CAP_BYTES
187    }
188
189    #[inline]
190    fn inline_meta_ptr(&self) -> *const u8 {
191        self as *const VString as *const u8
192    }
193
194    #[inline]
195    fn inline_data_ptr(&self) -> *const u8 {
196        unsafe { self.inline_meta_ptr().add(Self::INLINE_DATA_OFFSET) }
197    }
198
199    #[inline]
200    fn inline_len(&self) -> usize {
201        debug_assert!(self.is_inline());
202        unsafe { (*self.inline_meta_ptr() >> Self::INLINE_LEN_SHIFT) as usize }
203    }
204
205    #[cfg(feature = "alloc")]
206    fn new_inline(s: &str) -> Self {
207        debug_assert!(Self::can_inline(s.len()));
208        let mut storage = [0u8; Self::INLINE_WORD_BYTES];
209        storage[0] = ((s.len() as u8) << Self::INLINE_LEN_SHIFT) | (TypeTag::InlineString as u8);
210        storage[Self::INLINE_DATA_OFFSET..Self::INLINE_DATA_OFFSET + s.len()]
211            .copy_from_slice(s.as_bytes());
212        let bits = usize::from_ne_bytes(storage);
213        VString(unsafe { Value::from_bits(bits) })
214    }
215
216    /// Allocate a heap string with the safe flag set.
217    #[cfg(feature = "alloc")]
218    fn alloc_safe(s: &str) -> *mut StringHeader {
219        unsafe {
220            let layout = Self::layout(s.len());
221            let ptr = alloc(layout).cast::<StringHeader>();
222            (*ptr).len = s.len() | SAFE_FLAG;
223
224            // Copy string data
225            let data_ptr = ptr.add(1).cast::<u8>();
226            ptr::copy_nonoverlapping(s.as_ptr(), data_ptr, s.len());
227
228            ptr
229        }
230    }
231
232    /// Returns `true` if this string is marked as safe (e.g., pre-escaped HTML).
233    ///
234    /// Inline strings are never safe - only heap-allocated strings can carry the safe flag.
235    #[must_use]
236    pub fn is_safe(&self) -> bool {
237        if self.is_inline() {
238            false
239        } else {
240            self.header().is_safe()
241        }
242    }
243
244    /// Converts this string into a safe string.
245    ///
246    /// If the string is already safe, returns the same string wrapped as VSafeString.
247    /// If the string is inline, promotes it to heap storage with the safe flag.
248    /// If the string is on the heap but not safe, reallocates with the safe flag set.
249    #[cfg(feature = "alloc")]
250    #[must_use]
251    pub fn into_safe(self) -> VSafeString {
252        if self.is_safe() {
253            // Already safe, just wrap it
254            return VSafeString(self.0);
255        }
256        // Need to allocate (or reallocate) with safe flag
257        let s = self.as_str();
258        unsafe {
259            let ptr = Self::alloc_safe(s);
260            VSafeString(Value::new_ptr(ptr.cast(), TypeTag::StringOrNull))
261        }
262    }
263}
264
265const _: () = {
266    const_assert_eq!(VString::INLINE_DATA_OFFSET, 1);
267    const_assert!(
268        VString::INLINE_CAP_BYTES <= VString::INLINE_WORD_BYTES - VString::INLINE_DATA_OFFSET
269    );
270    const_assert!(VString::INLINE_LEN_MAX <= VString::INLINE_CAP_BYTES);
271};
272
273/// A string value marked as "safe" (e.g., pre-escaped HTML that should not be escaped again).
274///
275/// `VSafeString` is semantically a string, but carries a flag indicating it has already been
276/// processed (e.g., HTML-escaped) and should be output verbatim by template engines.
277///
278/// Unlike regular strings, safe strings are always heap-allocated since inline strings
279/// don't have room for the safe flag.
280///
281/// # Example use case
282///
283/// ```ignore
284/// // In a template engine:
285/// {{ page.content }}           // If VSafeString, output as-is
286/// {{ user_input }}             // Regular VString, escape HTML
287/// {{ user_input | safe }}      // Convert to VSafeString via into_safe()
288/// ```
289#[repr(transparent)]
290#[derive(Clone)]
291pub struct VSafeString(pub(crate) Value);
292
293impl VSafeString {
294    /// Creates a new safe string from a `&str`.
295    ///
296    /// This always heap-allocates, even for short strings, since the safe flag
297    /// is stored in the heap header.
298    #[cfg(feature = "alloc")]
299    #[must_use]
300    pub fn new(s: &str) -> Self {
301        unsafe {
302            let ptr = VString::alloc_safe(s);
303            VSafeString(Value::new_ptr(ptr.cast(), TypeTag::StringOrNull))
304        }
305    }
306
307    /// Returns the length of the string in bytes.
308    #[must_use]
309    pub fn len(&self) -> usize {
310        // Safe strings are never inline, so we can go directly to the header
311        self.header().actual_len()
312    }
313
314    /// Returns `true` if the string is empty.
315    #[must_use]
316    pub fn is_empty(&self) -> bool {
317        self.len() == 0
318    }
319
320    /// Returns the string as a `&str`.
321    #[must_use]
322    pub fn as_str(&self) -> &str {
323        unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
324    }
325
326    /// Returns the string as a byte slice.
327    #[must_use]
328    pub fn as_bytes(&self) -> &[u8] {
329        unsafe { core::slice::from_raw_parts(self.data_ptr(), self.len()) }
330    }
331
332    fn header(&self) -> &StringHeader {
333        unsafe { &*(self.0.heap_ptr() as *const StringHeader) }
334    }
335
336    fn data_ptr(&self) -> *const u8 {
337        unsafe { (self.0.heap_ptr() as *const StringHeader).add(1).cast() }
338    }
339}
340
341impl Deref for VSafeString {
342    type Target = str;
343
344    fn deref(&self) -> &str {
345        self.as_str()
346    }
347}
348
349impl Borrow<str> for VSafeString {
350    fn borrow(&self) -> &str {
351        self.as_str()
352    }
353}
354
355impl AsRef<str> for VSafeString {
356    fn as_ref(&self) -> &str {
357        self.as_str()
358    }
359}
360
361impl AsRef<[u8]> for VSafeString {
362    fn as_ref(&self) -> &[u8] {
363        self.as_bytes()
364    }
365}
366
367impl PartialEq for VSafeString {
368    fn eq(&self, other: &Self) -> bool {
369        self.as_str() == other.as_str()
370    }
371}
372
373impl Eq for VSafeString {}
374
375impl PartialOrd for VSafeString {
376    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
377        Some(self.cmp(other))
378    }
379}
380
381impl Ord for VSafeString {
382    fn cmp(&self, other: &Self) -> Ordering {
383        self.as_str().cmp(other.as_str())
384    }
385}
386
387impl Hash for VSafeString {
388    fn hash<H: Hasher>(&self, state: &mut H) {
389        self.as_str().hash(state);
390    }
391}
392
393impl Debug for VSafeString {
394    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
395        f.debug_tuple("SafeString").field(&self.as_str()).finish()
396    }
397}
398
399impl fmt::Display for VSafeString {
400    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
401        fmt::Display::fmt(self.as_str(), f)
402    }
403}
404
405// === PartialEq with str ===
406
407impl PartialEq<str> for VSafeString {
408    fn eq(&self, other: &str) -> bool {
409        self.as_str() == other
410    }
411}
412
413impl PartialEq<VSafeString> for str {
414    fn eq(&self, other: &VSafeString) -> bool {
415        self == other.as_str()
416    }
417}
418
419impl PartialEq<&str> for VSafeString {
420    fn eq(&self, other: &&str) -> bool {
421        self.as_str() == *other
422    }
423}
424
425#[cfg(feature = "alloc")]
426impl PartialEq<String> for VSafeString {
427    fn eq(&self, other: &String) -> bool {
428        self.as_str() == other.as_str()
429    }
430}
431
432#[cfg(feature = "alloc")]
433impl PartialEq<VString> for VSafeString {
434    fn eq(&self, other: &VString) -> bool {
435        self.as_str() == other.as_str()
436    }
437}
438
439#[cfg(feature = "alloc")]
440impl PartialEq<VSafeString> for VString {
441    fn eq(&self, other: &VSafeString) -> bool {
442        self.as_str() == other.as_str()
443    }
444}
445
446// === From implementations ===
447
448#[cfg(feature = "alloc")]
449impl From<&str> for VSafeString {
450    fn from(s: &str) -> Self {
451        Self::new(s)
452    }
453}
454
455#[cfg(feature = "alloc")]
456impl From<String> for VSafeString {
457    fn from(s: String) -> Self {
458        Self::new(&s)
459    }
460}
461
462#[cfg(feature = "alloc")]
463impl From<&String> for VSafeString {
464    fn from(s: &String) -> Self {
465        Self::new(s)
466    }
467}
468
469#[cfg(feature = "alloc")]
470impl From<VSafeString> for String {
471    fn from(s: VSafeString) -> Self {
472        s.as_str().into()
473    }
474}
475
476// A safe string IS a string, so we can convert
477impl From<VSafeString> for VString {
478    fn from(s: VSafeString) -> Self {
479        VString(s.0)
480    }
481}
482
483// === Value conversions ===
484
485impl AsRef<Value> for VSafeString {
486    fn as_ref(&self) -> &Value {
487        &self.0
488    }
489}
490
491impl AsMut<Value> for VSafeString {
492    fn as_mut(&mut self) -> &mut Value {
493        &mut self.0
494    }
495}
496
497impl From<VSafeString> for Value {
498    fn from(s: VSafeString) -> Self {
499        s.0
500    }
501}
502
503impl VSafeString {
504    /// Converts this VSafeString into a Value, consuming self.
505    #[inline]
506    pub fn into_value(self) -> Value {
507        self.0
508    }
509
510    /// Converts this VSafeString into a VString, consuming self.
511    /// The resulting VString will still have the safe flag set.
512    #[inline]
513    pub fn into_string(self) -> VString {
514        VString(self.0)
515    }
516}
517
518impl Deref for VString {
519    type Target = str;
520
521    fn deref(&self) -> &str {
522        self.as_str()
523    }
524}
525
526impl Borrow<str> for VString {
527    fn borrow(&self) -> &str {
528        self.as_str()
529    }
530}
531
532impl AsRef<str> for VString {
533    fn as_ref(&self) -> &str {
534        self.as_str()
535    }
536}
537
538impl AsRef<[u8]> for VString {
539    fn as_ref(&self) -> &[u8] {
540        self.as_bytes()
541    }
542}
543
544impl PartialEq for VString {
545    fn eq(&self, other: &Self) -> bool {
546        self.as_str() == other.as_str()
547    }
548}
549
550impl Eq for VString {}
551
552impl PartialOrd for VString {
553    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
554        Some(self.cmp(other))
555    }
556}
557
558impl Ord for VString {
559    fn cmp(&self, other: &Self) -> Ordering {
560        self.as_str().cmp(other.as_str())
561    }
562}
563
564impl Hash for VString {
565    fn hash<H: Hasher>(&self, state: &mut H) {
566        self.as_str().hash(state);
567    }
568}
569
570impl Debug for VString {
571    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
572        Debug::fmt(self.as_str(), f)
573    }
574}
575
576impl fmt::Display for VString {
577    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
578        fmt::Display::fmt(self.as_str(), f)
579    }
580}
581
582impl Default for VString {
583    fn default() -> Self {
584        Self::empty()
585    }
586}
587
588// === PartialEq with str ===
589
590impl PartialEq<str> for VString {
591    fn eq(&self, other: &str) -> bool {
592        self.as_str() == other
593    }
594}
595
596impl PartialEq<VString> for str {
597    fn eq(&self, other: &VString) -> bool {
598        self == other.as_str()
599    }
600}
601
602impl PartialEq<&str> for VString {
603    fn eq(&self, other: &&str) -> bool {
604        self.as_str() == *other
605    }
606}
607
608#[cfg(feature = "alloc")]
609impl PartialEq<String> for VString {
610    fn eq(&self, other: &String) -> bool {
611        self.as_str() == other.as_str()
612    }
613}
614
615#[cfg(feature = "alloc")]
616impl PartialEq<VString> for String {
617    fn eq(&self, other: &VString) -> bool {
618        self.as_str() == other.as_str()
619    }
620}
621
622// === From implementations ===
623
624#[cfg(feature = "alloc")]
625impl From<&str> for VString {
626    fn from(s: &str) -> Self {
627        Self::new(s)
628    }
629}
630
631#[cfg(feature = "alloc")]
632impl From<String> for VString {
633    fn from(s: String) -> Self {
634        Self::new(&s)
635    }
636}
637
638#[cfg(feature = "alloc")]
639impl From<&String> for VString {
640    fn from(s: &String) -> Self {
641        Self::new(s)
642    }
643}
644
645#[cfg(feature = "alloc")]
646impl From<VString> for String {
647    fn from(s: VString) -> Self {
648        s.as_str().into()
649    }
650}
651
652// === Value conversions ===
653
654impl AsRef<Value> for VString {
655    fn as_ref(&self) -> &Value {
656        &self.0
657    }
658}
659
660impl AsMut<Value> for VString {
661    fn as_mut(&mut self) -> &mut Value {
662        &mut self.0
663    }
664}
665
666impl From<VString> for Value {
667    fn from(s: VString) -> Self {
668        s.0
669    }
670}
671
672impl VString {
673    /// Converts this VString into a Value, consuming self.
674    #[inline]
675    pub fn into_value(self) -> Value {
676        self.0
677    }
678}
679
680#[cfg(feature = "alloc")]
681impl From<&str> for Value {
682    fn from(s: &str) -> Self {
683        VString::new(s).0
684    }
685}
686
687#[cfg(feature = "alloc")]
688impl From<String> for Value {
689    fn from(s: String) -> Self {
690        VString::new(&s).0
691    }
692}
693
694#[cfg(feature = "alloc")]
695impl From<&String> for Value {
696    fn from(s: &String) -> Self {
697        VString::new(s).0
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704    use crate::value::{TypeTag, Value};
705
706    #[test]
707    fn test_new() {
708        let s = VString::new("hello");
709        assert_eq!(s.as_str(), "hello");
710        assert_eq!(s.len(), 5);
711        assert!(!s.is_empty());
712    }
713
714    #[test]
715    fn test_empty() {
716        let s = VString::empty();
717        assert_eq!(s.as_str(), "");
718        assert_eq!(s.len(), 0);
719        assert!(s.is_empty());
720    }
721
722    #[test]
723    fn test_equality() {
724        let a = VString::new("hello");
725        let b = VString::new("hello");
726        let c = VString::new("world");
727
728        assert_eq!(a, b);
729        assert_ne!(a, c);
730        assert_eq!(a, "hello");
731        assert_eq!(a.as_str(), "hello");
732    }
733
734    #[test]
735    fn test_clone() {
736        let a = VString::new("test");
737        let b = a.clone();
738        assert_eq!(a, b);
739    }
740
741    #[test]
742    fn test_unicode() {
743        let s = VString::new("hello δΈ–η•Œ 🌍");
744        assert_eq!(s.as_str(), "hello δΈ–η•Œ 🌍");
745    }
746
747    #[test]
748    fn test_deref() {
749        let s = VString::new("hello");
750        assert!(s.starts_with("hel"));
751        assert!(s.ends_with("llo"));
752    }
753
754    #[test]
755    fn test_ordering() {
756        let a = VString::new("apple");
757        let b = VString::new("banana");
758        assert!(a < b);
759    }
760
761    #[test]
762    fn test_inline_representation() {
763        let s = VString::new("inline");
764        assert!(s.is_inline(), "expected inline storage");
765        assert_eq!(s.as_str(), "inline");
766    }
767
768    #[test]
769    fn test_heap_representation() {
770        let long_input = "a".repeat(VString::INLINE_LEN_MAX + 1);
771        let s = VString::new(&long_input);
772        assert!(!s.is_inline(), "expected heap storage");
773        assert_eq!(s.as_str(), long_input);
774    }
775
776    #[test]
777    fn inline_capacity_boundaries() {
778        for len in 0..=VString::INLINE_LEN_MAX {
779            let input = "x".repeat(len);
780            let s = VString::new(&input);
781            assert!(
782                s.is_inline(),
783                "expected inline storage for length {} (capacity {})",
784                len,
785                VString::INLINE_LEN_MAX
786            );
787            assert_eq!(s.len(), len);
788            assert_eq!(s.as_str(), input);
789            assert_eq!(s.as_bytes(), input.as_bytes());
790        }
791
792        let overflow = "y".repeat(VString::INLINE_LEN_MAX + 1);
793        let heap = VString::new(&overflow);
794        assert!(
795            !heap.is_inline(),
796            "length {} should force heap allocation",
797            overflow.len()
798        );
799    }
800
801    #[test]
802    fn inline_value_tag_matches() {
803        for len in 0..=VString::INLINE_LEN_MAX {
804            let input = "z".repeat(len);
805            let value = Value::from(input.as_str());
806            assert!(value.is_inline_string(), "Value should mark inline string");
807            assert_eq!(
808                value.ptr_usize() & 0b111,
809                TypeTag::InlineString as usize,
810                "low bits must store inline string tag"
811            );
812            let roundtrip = value.as_string().expect("string value");
813            assert_eq!(roundtrip.as_str(), input);
814            assert_eq!(roundtrip.as_bytes(), input.as_bytes());
815        }
816    }
817
818    #[cfg(target_pointer_width = "64")]
819    #[test]
820    fn inline_len_max_is_seven_on_64_bit() {
821        assert_eq!(VString::INLINE_LEN_MAX, 7);
822    }
823
824    #[cfg(target_pointer_width = "32")]
825    #[test]
826    fn inline_len_max_is_three_on_32_bit() {
827        assert_eq!(VString::INLINE_LEN_MAX, 3);
828    }
829
830    // === VSafeString tests ===
831
832    #[test]
833    fn test_safe_string_new() {
834        let s = VSafeString::new("hello");
835        assert_eq!(s.as_str(), "hello");
836        assert_eq!(s.len(), 5);
837        assert!(!s.is_empty());
838    }
839
840    #[test]
841    fn test_safe_string_roundtrip() {
842        let original = "<b>bold</b>";
843        let safe = VSafeString::new(original);
844        assert_eq!(safe.as_str(), original);
845    }
846
847    #[test]
848    fn test_safe_string_is_always_heap() {
849        // Even short strings should be heap-allocated for safe strings
850        let short = VSafeString::new("hi");
851        assert_eq!(short.len(), 2);
852        assert_eq!(short.as_str(), "hi");
853        // The value should have tag 1 (StringOrNull) not tag 6 (InlineString)
854        let value: Value = short.into();
855        assert!(!value.is_inline_string());
856        assert!(value.is_string());
857    }
858
859    #[test]
860    fn test_vstring_is_safe() {
861        let normal = VString::new("hello");
862        assert!(!normal.is_safe());
863
864        let safe = VSafeString::new("hello");
865        // When viewed as VString, should still report safe
866        let as_vstring: VString = safe.into();
867        assert!(as_vstring.is_safe());
868    }
869
870    #[test]
871    fn test_vstring_into_safe() {
872        // Test inline string promotion
873        let inline = VString::new("hi");
874        assert!(inline.is_inline());
875        let safe = inline.into_safe();
876        assert_eq!(safe.as_str(), "hi");
877
878        // Test heap string conversion
879        let long = "a".repeat(VString::INLINE_LEN_MAX + 10);
880        let heap = VString::new(&long);
881        assert!(!heap.is_inline());
882        let safe_heap = heap.into_safe();
883        assert_eq!(safe_heap.as_str(), long);
884    }
885
886    #[test]
887    fn test_safe_flag_preserved_through_clone() {
888        let safe = VSafeString::new("<b>bold</b>");
889        let value: Value = safe.into();
890        assert!(value.is_safe_string());
891
892        let cloned = value.clone();
893        assert!(cloned.is_safe_string());
894        assert_eq!(cloned.as_string().unwrap().as_str(), "<b>bold</b>");
895    }
896
897    #[test]
898    fn test_value_as_safe_string() {
899        let safe = VSafeString::new("safe content");
900        let value: Value = safe.into();
901
902        // is_string should return true (safe strings ARE strings)
903        assert!(value.is_string());
904        // is_safe_string should also return true
905        assert!(value.is_safe_string());
906        // as_string should work
907        assert_eq!(value.as_string().unwrap().as_str(), "safe content");
908        // as_safe_string should work
909        assert_eq!(value.as_safe_string().unwrap().as_str(), "safe content");
910    }
911
912    #[test]
913    fn test_normal_string_not_safe() {
914        let normal = VString::new("normal");
915        let value: Value = normal.into();
916
917        assert!(value.is_string());
918        assert!(!value.is_safe_string());
919        assert!(value.as_string().is_some());
920        assert!(value.as_safe_string().is_none());
921    }
922
923    #[test]
924    fn test_safe_string_equality() {
925        let a = VSafeString::new("hello");
926        let b = VSafeString::new("hello");
927        let c = VSafeString::new("world");
928
929        assert_eq!(a, b);
930        assert_ne!(a, c);
931        assert_eq!(a, "hello");
932
933        // Equality with VString
934        let vstring = VString::new("hello");
935        assert_eq!(a, vstring);
936        assert_eq!(vstring, a);
937    }
938
939    #[test]
940    fn test_safe_string_into_string() {
941        let safe = VSafeString::new("test");
942        let vstring = safe.into_string();
943        assert_eq!(vstring.as_str(), "test");
944        assert!(vstring.is_safe()); // Flag should be preserved
945    }
946
947    #[test]
948    fn test_safe_flag_constant() {
949        // Verify the safe flag uses the high bit
950        assert_eq!(SAFE_FLAG, 1usize << (usize::BITS - 1));
951        // On 64-bit: 0x8000_0000_0000_0000
952        // On 32-bit: 0x8000_0000
953    }
954
955    #[test]
956    fn test_safe_string_long() {
957        // Test with a string that would definitely be heap-allocated anyway
958        let long = "a".repeat(1000);
959        let safe = VSafeString::new(&long);
960        assert_eq!(safe.len(), 1000);
961        assert_eq!(safe.as_str(), long);
962
963        let value: Value = safe.into();
964        assert!(value.is_safe_string());
965        assert_eq!(value.as_string().unwrap().len(), 1000);
966    }
967}
968
969#[cfg(all(test, feature = "bolero-inline-tests"))]
970mod bolero_props {
971    use super::*;
972    use crate::ValueType;
973    use crate::array::VArray;
974    use alloc::string::String;
975    use alloc::vec::Vec;
976    use bolero::check;
977
978    #[test]
979    fn bolero_inline_string_round_trip() {
980        check!().with_type::<Vec<u8>>().for_each(|bytes: &Vec<u8>| {
981            if bytes.len() > VString::INLINE_LEN_MAX + 8 {
982                // Keep the generator focused on short payloads to hit inline cases hard.
983                return;
984            }
985
986            if let Ok(s) = String::from_utf8(bytes.clone()) {
987                let value = Value::from(s.as_str());
988                let roundtrip = value.as_string().expect("expected string value");
989                assert_eq!(roundtrip.as_str(), s);
990
991                if VString::can_inline(s.len()) {
992                    assert!(value.is_inline_string(), "expected inline tag for {s:?}");
993                } else {
994                    assert!(!value.is_inline_string(), "unexpected inline tag for {s:?}");
995                }
996            }
997        });
998    }
999
1000    #[test]
1001    fn bolero_string_mutation_sequences() {
1002        check!().with_type::<Vec<u8>>().for_each(|bytes: &Vec<u8>| {
1003            let mut value = Value::from("");
1004            let mut expected = String::new();
1005
1006            for chunk in bytes.chunks(3).take(24) {
1007                let selector = chunk.first().copied().unwrap_or(0) % 3;
1008                match selector {
1009                    0 => {
1010                        let ch = (b'a' + chunk.get(1).copied().unwrap_or(0) % 26) as char;
1011                        expected.push(ch);
1012                    }
1013                    1 => {
1014                        if !expected.is_empty() {
1015                            let len = chunk
1016                                .get(1)
1017                                .copied()
1018                                .map(|n| (n as usize) % expected.len())
1019                                .unwrap_or(0);
1020                            expected.truncate(len);
1021                        }
1022                    }
1023                    _ => expected.clear(),
1024                }
1025
1026                overwrite_value_string(&mut value, &expected);
1027                assert_eq!(value.as_string().unwrap().as_str(), expected);
1028                assert_eq!(
1029                    value.is_inline_string(),
1030                    expected.len() <= VString::INLINE_LEN_MAX,
1031                    "mutation sequence should keep inline status accurate"
1032                );
1033            }
1034        });
1035    }
1036
1037    #[test]
1038    fn bolero_array_model_matches() {
1039        check!().with_type::<Vec<u8>>().for_each(|bytes: &Vec<u8>| {
1040            let mut arr = VArray::new();
1041            let mut model: Vec<String> = Vec::new();
1042
1043            for chunk in bytes.chunks(4).take(20) {
1044                match chunk.first().copied().unwrap_or(0) % 4 {
1045                    0 => {
1046                        let content = inline_string_from_chunk(chunk, 1);
1047                        arr.push(Value::from(content.as_str()));
1048                        model.push(content);
1049                    }
1050                    1 => {
1051                        let idx = chunk.get(1).copied().unwrap_or(0) as usize;
1052                        if !model.is_empty() {
1053                            let idx = idx % model.len();
1054                            model.remove(idx);
1055                            let _ = arr.remove(idx);
1056                        }
1057                    }
1058                    2 => {
1059                        let content = inline_string_from_chunk(chunk, 2);
1060                        if model.is_empty() {
1061                            arr.insert(0, Value::from(content.as_str()));
1062                            model.insert(0, content);
1063                        } else {
1064                            let len = model.len();
1065                            let idx = (chunk.get(2).copied().unwrap_or(0) as usize) % (len + 1);
1066                            arr.insert(idx, Value::from(content.as_str()));
1067                            model.insert(idx, content);
1068                        }
1069                    }
1070                    _ => {
1071                        arr.clear();
1072                        model.clear();
1073                    }
1074                }
1075
1076                assert_eq!(arr.len(), model.len());
1077                for (value, expected) in arr.iter().zip(model.iter()) {
1078                    assert_eq!(value.value_type(), ValueType::String);
1079                    assert_eq!(value.as_string().unwrap().as_str(), expected);
1080                    assert_eq!(
1081                        value.is_inline_string(),
1082                        expected.len() <= VString::INLINE_LEN_MAX
1083                    );
1084                }
1085            }
1086        });
1087    }
1088
1089    fn overwrite_value_string(value: &mut Value, new_value: &str) {
1090        let slot = value.as_string_mut().expect("expected string value");
1091        *slot = VString::new(new_value);
1092    }
1093
1094    fn inline_string_from_chunk(chunk: &[u8], seed_idx: usize) -> String {
1095        let len_hint = chunk.get(seed_idx).copied().unwrap_or(0) as usize;
1096        let len = len_hint % (VString::INLINE_LEN_MAX.saturating_sub(1).max(1));
1097        (0..len)
1098            .map(|i| {
1099                let byte = chunk.get(i % chunk.len()).copied().unwrap_or(b'a');
1100                (b'a' + (byte % 26)) as char
1101            })
1102            .collect()
1103    }
1104}