facet_value/
string.rs

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