• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 20188989810

13 Dec 2025 07:42AM UTC coverage: 57.967% (-0.2%) from 58.211%
20188989810

push

github

fasterthanlime
Add value-based probing for out-of-order tagged enum fields

Extends FieldEvidence with optional scalar_value capture, enabling
internally and adjacently tagged enums to work regardless of field order.

Changes:
- Add scalar_value field to FieldEvidence for capturing tag values during probing
- Update JSON build_probe to capture scalar values instead of skipping
- Update XML build_probe to properly scan remaining events for field evidence
- Rewrite internally/adjacently tagged enum deserialization to probe first
- Add out-of-order tests for both JSON and XML

Fixes cases like {"radius": 5.0, "type": "Circle"} where tag comes last.

Refs: #1271

117 of 205 new or added lines in 4 files covered. (57.07%)

568 existing lines in 4 files now uncovered.

32370 of 55842 relevant lines covered (57.97%)

5679.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.38
/facet-value/src/string.rs
1
//! String value type.
2

3
#[cfg(feature = "alloc")]
4
use alloc::alloc::{Layout, alloc, dealloc};
5
#[cfg(feature = "alloc")]
6
use alloc::string::String;
7
use core::borrow::Borrow;
8
use core::cmp::Ordering;
9
use core::fmt::{self, Debug, Formatter};
10
use core::hash::{Hash, Hasher};
11
use core::mem;
12
use core::ops::Deref;
13
use core::ptr;
14
use static_assertions::const_assert;
15
use static_assertions::const_assert_eq;
16

17
use 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.
21
const SAFE_FLAG: usize = 1usize << (usize::BITS - 1);
22

23
/// Header for heap-allocated strings.
24
#[repr(C, align(8))]
25
struct 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

32
impl StringHeader {
33
    /// Returns the actual length of the string, masking out the safe flag.
34
    #[inline]
35
    fn actual_len(&self) -> usize {
109✔
36
        self.len & !SAFE_FLAG
109✔
37
    }
109✔
38

39
    /// Returns true if the safe flag is set.
40
    #[inline]
41
    fn is_safe(&self) -> bool {
9✔
42
        self.len & SAFE_FLAG != 0
9✔
43
    }
9✔
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)]
52
pub struct VString(pub(crate) Value);
53

54
impl 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 {
80✔
66
        Layout::new::<StringHeader>()
80✔
67
            .extend(Layout::array::<u8>(len).unwrap())
80✔
68
            .unwrap()
80✔
69
            .0
80✔
70
            .pad_to_align()
80✔
71
    }
80✔
72

73
    #[cfg(feature = "alloc")]
74
    fn alloc(s: &str) -> *mut StringHeader {
26✔
75
        unsafe {
76
            let layout = Self::layout(s.len());
26✔
77
            let ptr = alloc(layout).cast::<StringHeader>();
26✔
78
            (*ptr).len = s.len();
26✔
79

80
            // Copy string data
81
            let data_ptr = ptr.add(1).cast::<u8>();
26✔
82
            ptr::copy_nonoverlapping(s.as_ptr(), data_ptr, s.len());
26✔
83

84
            ptr
26✔
85
        }
86
    }
26✔
87

88
    #[cfg(feature = "alloc")]
89
    fn dealloc_ptr(ptr: *mut StringHeader) {
40✔
90
        unsafe {
40✔
91
            let len = (*ptr).actual_len();
40✔
92
            let layout = Self::layout(len);
40✔
93
            dealloc(ptr.cast::<u8>(), layout);
40✔
94
        }
40✔
95
    }
40✔
96

97
    fn header(&self) -> &StringHeader {
60✔
98
        debug_assert!(!self.is_inline());
60✔
99
        unsafe { &*(self.0.heap_ptr() as *const StringHeader) }
60✔
100
    }
60✔
101

102
    fn data_ptr(&self) -> *const u8 {
50✔
103
        debug_assert!(!self.is_inline());
50✔
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() }
50✔
107
    }
50✔
108

109
    /// Creates a new string from a `&str`.
110
    #[cfg(feature = "alloc")]
111
    #[must_use]
112
    pub fn new(s: &str) -> Self {
483✔
113
        if Self::can_inline(s.len()) {
483✔
114
            return Self::new_inline(s);
457✔
115
        }
26✔
116
        unsafe {
117
            let ptr = Self::alloc(s);
26✔
118
            VString(Value::new_ptr(ptr.cast(), TypeTag::StringOrNull))
26✔
119
        }
120
    }
483✔
121

122
    /// Creates an empty string.
123
    #[cfg(feature = "alloc")]
124
    #[must_use]
125
    pub fn empty() -> Self {
1✔
126
        Self::new_inline("")
1✔
127
    }
1✔
128

129
    /// Returns the length of the string in bytes.
130
    #[must_use]
131
    pub fn len(&self) -> usize {
63✔
132
        if self.is_inline() {
63✔
133
            self.inline_len()
12✔
134
        } else {
135
            self.header().actual_len()
51✔
136
        }
137
    }
63✔
138

139
    /// Returns `true` if the string is empty.
140
    #[must_use]
141
    pub fn is_empty(&self) -> bool {
2✔
142
        self.len() == 0
2✔
143
    }
2✔
144

145
    /// Returns the string as a `&str`.
146
    #[must_use]
147
    pub fn as_str(&self) -> &str {
1,351✔
148
        unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
1,351✔
149
    }
1,351✔
150

151
    /// Returns the string as a byte slice.
152
    #[must_use]
153
    pub fn as_bytes(&self) -> &[u8] {
1,367✔
154
        if self.is_inline() {
1,367✔
155
            unsafe { core::slice::from_raw_parts(self.inline_data_ptr(), self.inline_len()) }
1,317✔
156
        } else {
157
            unsafe { core::slice::from_raw_parts(self.data_ptr(), self.len()) }
50✔
158
        }
159
    }
1,367✔
160

161
    pub(crate) fn clone_impl(&self) -> Value {
31✔
162
        if self.is_safe() {
31✔
163
            // Preserve the safe flag through clone
164
            VSafeString::new(self.as_str()).0
1✔
165
        } else {
166
            VString::new(self.as_str()).0
30✔
167
        }
168
    }
31✔
169

170
    pub(crate) fn drop_impl(&mut self) {
498✔
171
        if self.is_inline() {
498✔
172
            return;
458✔
173
        }
40✔
174
        unsafe {
40✔
175
            Self::dealloc_ptr(self.0.heap_ptr_mut().cast());
40✔
176
        }
40✔
177
    }
498✔
178

179
    #[inline]
180
    fn is_inline(&self) -> bool {
3,423✔
181
        self.0.is_inline_string()
3,423✔
182
    }
3,423✔
183

184
    #[inline]
185
    fn can_inline(len: usize) -> bool {
941✔
186
        len <= Self::INLINE_LEN_MAX && len <= Self::INLINE_CAP_BYTES
941✔
187
    }
941✔
188

189
    #[inline]
190
    fn inline_meta_ptr(&self) -> *const u8 {
2,646✔
191
        self as *const VString as *const u8
2,646✔
192
    }
2,646✔
193

194
    #[inline]
195
    fn inline_data_ptr(&self) -> *const u8 {
1,317✔
196
        unsafe { self.inline_meta_ptr().add(Self::INLINE_DATA_OFFSET) }
1,317✔
197
    }
1,317✔
198

199
    #[inline]
200
    fn inline_len(&self) -> usize {
1,329✔
201
        debug_assert!(self.is_inline());
1,329✔
202
        unsafe { (*self.inline_meta_ptr() >> Self::INLINE_LEN_SHIFT) as usize }
1,329✔
203
    }
1,329✔
204

205
    #[cfg(feature = "alloc")]
206
    fn new_inline(s: &str) -> Self {
458✔
207
        debug_assert!(Self::can_inline(s.len()));
458✔
208
        let mut storage = [0u8; Self::INLINE_WORD_BYTES];
458✔
209
        storage[0] = ((s.len() as u8) << Self::INLINE_LEN_SHIFT) | (TypeTag::InlineString as u8);
458✔
210
        storage[Self::INLINE_DATA_OFFSET..Self::INLINE_DATA_OFFSET + s.len()]
458✔
211
            .copy_from_slice(s.as_bytes());
458✔
212
        let bits = usize::from_ne_bytes(storage);
458✔
213
        VString(unsafe { Value::from_bits(bits) })
458✔
214
    }
458✔
215

216
    /// Allocate a heap string with the safe flag set.
217
    #[cfg(feature = "alloc")]
218
    fn alloc_safe(s: &str) -> *mut StringHeader {
14✔
219
        unsafe {
220
            let layout = Self::layout(s.len());
14✔
221
            let ptr = alloc(layout).cast::<StringHeader>();
14✔
222
            (*ptr).len = s.len() | SAFE_FLAG;
14✔
223

224
            // Copy string data
225
            let data_ptr = ptr.add(1).cast::<u8>();
14✔
226
            ptr::copy_nonoverlapping(s.as_ptr(), data_ptr, s.len());
14✔
227

228
            ptr
14✔
229
        }
230
    }
14✔
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 {
43✔
237
        if self.is_inline() {
43✔
238
            false
34✔
239
        } else {
240
            self.header().is_safe()
9✔
241
        }
242
    }
43✔
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 {
2✔
252
        if self.is_safe() {
2✔
253
            // Already safe, just wrap it
254
            return VSafeString(self.0);
×
255
        }
2✔
256
        // Need to allocate (or reallocate) with safe flag
257
        let s = self.as_str();
2✔
258
        unsafe {
259
            let ptr = Self::alloc_safe(s);
2✔
260
            VSafeString(Value::new_ptr(ptr.cast(), TypeTag::StringOrNull))
2✔
261
        }
262
    }
2✔
263
}
264

265
const _: () = {
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)]
291
pub struct VSafeString(pub(crate) Value);
292

293
impl 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 {
12✔
301
        unsafe {
302
            let ptr = VString::alloc_safe(s);
12✔
303
            VSafeString(Value::new_ptr(ptr.cast(), TypeTag::StringOrNull))
12✔
304
        }
305
    }
12✔
306

307
    /// Returns the length of the string in bytes.
308
    #[must_use]
309
    pub fn len(&self) -> usize {
18✔
310
        // Safe strings are never inline, so we can go directly to the header
311
        self.header().actual_len()
18✔
312
    }
18✔
313

314
    /// Returns `true` if the string is empty.
315
    #[must_use]
316
    pub fn is_empty(&self) -> bool {
1✔
317
        self.len() == 0
1✔
318
    }
1✔
319

320
    /// Returns the string as a `&str`.
321
    #[must_use]
322
    pub fn as_str(&self) -> &str {
14✔
323
        unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
14✔
324
    }
14✔
325

326
    /// Returns the string as a byte slice.
327
    #[must_use]
328
    pub fn as_bytes(&self) -> &[u8] {
14✔
329
        unsafe { core::slice::from_raw_parts(self.data_ptr(), self.len()) }
14✔
330
    }
14✔
331

332
    fn header(&self) -> &StringHeader {
18✔
333
        unsafe { &*(self.0.heap_ptr() as *const StringHeader) }
18✔
334
    }
18✔
335

336
    fn data_ptr(&self) -> *const u8 {
14✔
337
        unsafe { (self.0.heap_ptr() as *const StringHeader).add(1).cast() }
14✔
338
    }
14✔
339
}
340

341
impl Deref for VSafeString {
342
    type Target = str;
343

344
    fn deref(&self) -> &str {
×
345
        self.as_str()
×
346
    }
×
347
}
348

349
impl Borrow<str> for VSafeString {
350
    fn borrow(&self) -> &str {
×
351
        self.as_str()
×
352
    }
×
353
}
354

355
impl AsRef<str> for VSafeString {
356
    fn as_ref(&self) -> &str {
×
357
        self.as_str()
×
358
    }
×
359
}
360

361
impl AsRef<[u8]> for VSafeString {
362
    fn as_ref(&self) -> &[u8] {
×
363
        self.as_bytes()
×
364
    }
×
365
}
366

367
impl PartialEq for VSafeString {
368
    fn eq(&self, other: &Self) -> bool {
2✔
369
        self.as_str() == other.as_str()
2✔
370
    }
2✔
371
}
372

373
impl Eq for VSafeString {}
374

375
impl PartialOrd for VSafeString {
376
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
×
377
        Some(self.cmp(other))
×
378
    }
×
379
}
380

381
impl Ord for VSafeString {
382
    fn cmp(&self, other: &Self) -> Ordering {
×
383
        self.as_str().cmp(other.as_str())
×
384
    }
×
385
}
386

387
impl Hash for VSafeString {
388
    fn hash<H: Hasher>(&self, state: &mut H) {
×
389
        self.as_str().hash(state);
×
390
    }
×
391
}
392

393
impl 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

399
impl 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

407
impl PartialEq<str> for VSafeString {
408
    fn eq(&self, other: &str) -> bool {
×
409
        self.as_str() == other
×
410
    }
×
411
}
412

413
impl PartialEq<VSafeString> for str {
414
    fn eq(&self, other: &VSafeString) -> bool {
×
415
        self == other.as_str()
×
416
    }
×
417
}
418

419
impl PartialEq<&str> for VSafeString {
420
    fn eq(&self, other: &&str) -> bool {
1✔
421
        self.as_str() == *other
1✔
422
    }
1✔
423
}
424

425
#[cfg(feature = "alloc")]
426
impl 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")]
433
impl PartialEq<VString> for VSafeString {
434
    fn eq(&self, other: &VString) -> bool {
1✔
435
        self.as_str() == other.as_str()
1✔
436
    }
1✔
437
}
438

439
#[cfg(feature = "alloc")]
440
impl PartialEq<VSafeString> for VString {
441
    fn eq(&self, other: &VSafeString) -> bool {
1✔
442
        self.as_str() == other.as_str()
1✔
443
    }
1✔
444
}
445

446
// === From implementations ===
447

448
#[cfg(feature = "alloc")]
449
impl From<&str> for VSafeString {
450
    fn from(s: &str) -> Self {
×
451
        Self::new(s)
×
452
    }
×
453
}
454

455
#[cfg(feature = "alloc")]
456
impl From<String> for VSafeString {
457
    fn from(s: String) -> Self {
×
458
        Self::new(&s)
×
459
    }
×
460
}
461

462
#[cfg(feature = "alloc")]
463
impl From<&String> for VSafeString {
464
    fn from(s: &String) -> Self {
×
465
        Self::new(s)
×
466
    }
×
467
}
468

469
#[cfg(feature = "alloc")]
470
impl 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
477
impl From<VSafeString> for VString {
478
    fn from(s: VSafeString) -> Self {
1✔
479
        VString(s.0)
1✔
480
    }
1✔
481
}
482

483
// === Value conversions ===
484

485
impl AsRef<Value> for VSafeString {
486
    fn as_ref(&self) -> &Value {
×
487
        &self.0
×
488
    }
×
489
}
490

491
impl AsMut<Value> for VSafeString {
492
    fn as_mut(&mut self) -> &mut Value {
×
493
        &mut self.0
×
494
    }
×
495
}
496

497
impl From<VSafeString> for Value {
498
    fn from(s: VSafeString) -> Self {
4✔
499
        s.0
4✔
500
    }
4✔
501
}
502

503
impl 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 {
1✔
514
        VString(self.0)
1✔
515
    }
1✔
516
}
517

518
impl Deref for VString {
519
    type Target = str;
520

521
    fn deref(&self) -> &str {
2✔
522
        self.as_str()
2✔
523
    }
2✔
524
}
525

526
impl Borrow<str> for VString {
527
    fn borrow(&self) -> &str {
×
528
        self.as_str()
×
529
    }
×
530
}
531

532
impl AsRef<str> for VString {
533
    fn as_ref(&self) -> &str {
×
534
        self.as_str()
×
535
    }
×
536
}
537

538
impl AsRef<[u8]> for VString {
539
    fn as_ref(&self) -> &[u8] {
×
540
        self.as_bytes()
×
541
    }
×
542
}
543

544
impl PartialEq for VString {
545
    fn eq(&self, other: &Self) -> bool {
71✔
546
        self.as_str() == other.as_str()
71✔
547
    }
71✔
548
}
549

550
impl Eq for VString {}
551

552
impl PartialOrd for VString {
553
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1✔
554
        Some(self.cmp(other))
1✔
555
    }
1✔
556
}
557

558
impl Ord for VString {
559
    fn cmp(&self, other: &Self) -> Ordering {
1✔
560
        self.as_str().cmp(other.as_str())
1✔
561
    }
1✔
562
}
563

564
impl Hash for VString {
565
    fn hash<H: Hasher>(&self, state: &mut H) {
×
566
        self.as_str().hash(state);
×
567
    }
×
568
}
569

570
impl Debug for VString {
571
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2✔
572
        Debug::fmt(self.as_str(), f)
2✔
573
    }
2✔
574
}
575

576
impl fmt::Display for VString {
577
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2✔
578
        fmt::Display::fmt(self.as_str(), f)
2✔
579
    }
2✔
580
}
581

582
impl Default for VString {
583
    fn default() -> Self {
×
584
        Self::empty()
×
585
    }
×
586
}
587

588
// === PartialEq with str ===
589

590
impl PartialEq<str> for VString {
591
    fn eq(&self, other: &str) -> bool {
×
592
        self.as_str() == other
×
593
    }
×
594
}
595

596
impl PartialEq<VString> for str {
597
    fn eq(&self, other: &VString) -> bool {
×
598
        self == other.as_str()
×
599
    }
×
600
}
601

602
impl PartialEq<&str> for VString {
603
    fn eq(&self, other: &&str) -> bool {
1✔
604
        self.as_str() == *other
1✔
605
    }
1✔
606
}
607

608
#[cfg(feature = "alloc")]
609
impl 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")]
616
impl 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")]
625
impl From<&str> for VString {
626
    fn from(s: &str) -> Self {
259✔
627
        Self::new(s)
259✔
628
    }
259✔
629
}
630

631
#[cfg(feature = "alloc")]
632
impl From<String> for VString {
UNCOV
633
    fn from(s: String) -> Self {
×
UNCOV
634
        Self::new(&s)
×
UNCOV
635
    }
×
636
}
637

638
#[cfg(feature = "alloc")]
639
impl From<&String> for VString {
640
    fn from(s: &String) -> Self {
×
641
        Self::new(s)
×
642
    }
×
643
}
644

645
#[cfg(feature = "alloc")]
646
impl From<VString> for String {
647
    fn from(s: VString) -> Self {
×
648
        s.as_str().into()
×
649
    }
×
650
}
651

652
// === Value conversions ===
653

654
impl AsRef<Value> for VString {
655
    fn as_ref(&self) -> &Value {
×
656
        &self.0
×
657
    }
×
658
}
659

660
impl AsMut<Value> for VString {
661
    fn as_mut(&mut self) -> &mut Value {
×
662
        &mut self.0
×
663
    }
×
664
}
665

666
impl From<VString> for Value {
667
    fn from(s: VString) -> Self {
15✔
668
        s.0
15✔
669
    }
15✔
670
}
671

672
impl VString {
673
    /// Converts this VString into a Value, consuming self.
674
    #[inline]
675
    pub fn into_value(self) -> Value {
11✔
676
        self.0
11✔
677
    }
11✔
678
}
679

680
#[cfg(feature = "alloc")]
681
impl From<&str> for Value {
682
    fn from(s: &str) -> Self {
142✔
683
        VString::new(s).0
142✔
684
    }
142✔
685
}
686

687
#[cfg(feature = "alloc")]
688
impl From<String> for Value {
UNCOV
689
    fn from(s: String) -> Self {
×
UNCOV
690
        VString::new(&s).0
×
UNCOV
691
    }
×
692
}
693

694
#[cfg(feature = "alloc")]
695
impl From<&String> for Value {
696
    fn from(s: &String) -> Self {
×
697
        VString::new(s).0
×
698
    }
×
699
}
700

701
#[cfg(test)]
702
mod tests {
703
    use super::*;
704
    use crate::value::{TypeTag, Value};
705

706
    #[test]
707
    fn test_new() {
1✔
708
        let s = VString::new("hello");
1✔
709
        assert_eq!(s.as_str(), "hello");
1✔
710
        assert_eq!(s.len(), 5);
1✔
711
        assert!(!s.is_empty());
1✔
712
    }
1✔
713

714
    #[test]
715
    fn test_empty() {
1✔
716
        let s = VString::empty();
1✔
717
        assert_eq!(s.as_str(), "");
1✔
718
        assert_eq!(s.len(), 0);
1✔
719
        assert!(s.is_empty());
1✔
720
    }
1✔
721

722
    #[test]
723
    fn test_equality() {
1✔
724
        let a = VString::new("hello");
1✔
725
        let b = VString::new("hello");
1✔
726
        let c = VString::new("world");
1✔
727

728
        assert_eq!(a, b);
1✔
729
        assert_ne!(a, c);
1✔
730
        assert_eq!(a, "hello");
1✔
731
        assert_eq!(a.as_str(), "hello");
1✔
732
    }
1✔
733

734
    #[test]
735
    fn test_clone() {
1✔
736
        let a = VString::new("test");
1✔
737
        let b = a.clone();
1✔
738
        assert_eq!(a, b);
1✔
739
    }
1✔
740

741
    #[test]
742
    fn test_unicode() {
1✔
743
        let s = VString::new("hello 世界 🌍");
1✔
744
        assert_eq!(s.as_str(), "hello 世界 🌍");
1✔
745
    }
1✔
746

747
    #[test]
748
    fn test_deref() {
1✔
749
        let s = VString::new("hello");
1✔
750
        assert!(s.starts_with("hel"));
1✔
751
        assert!(s.ends_with("llo"));
1✔
752
    }
1✔
753

754
    #[test]
755
    fn test_ordering() {
1✔
756
        let a = VString::new("apple");
1✔
757
        let b = VString::new("banana");
1✔
758
        assert!(a < b);
1✔
759
    }
1✔
760

761
    #[test]
762
    fn test_inline_representation() {
1✔
763
        let s = VString::new("inline");
1✔
764
        assert!(s.is_inline(), "expected inline storage");
1✔
765
        assert_eq!(s.as_str(), "inline");
1✔
766
    }
1✔
767

768
    #[test]
769
    fn test_heap_representation() {
1✔
770
        let long_input = "a".repeat(VString::INLINE_LEN_MAX + 1);
1✔
771
        let s = VString::new(&long_input);
1✔
772
        assert!(!s.is_inline(), "expected heap storage");
1✔
773
        assert_eq!(s.as_str(), long_input);
1✔
774
    }
1✔
775

776
    #[test]
777
    fn inline_capacity_boundaries() {
1✔
778
        for len in 0..=VString::INLINE_LEN_MAX {
8✔
779
            let input = "x".repeat(len);
8✔
780
            let s = VString::new(&input);
8✔
781
            assert!(
8✔
782
                s.is_inline(),
8✔
783
                "expected inline storage for length {} (capacity {})",
784
                len,
785
                VString::INLINE_LEN_MAX
786
            );
787
            assert_eq!(s.len(), len);
8✔
788
            assert_eq!(s.as_str(), input);
8✔
789
            assert_eq!(s.as_bytes(), input.as_bytes());
8✔
790
        }
791

792
        let overflow = "y".repeat(VString::INLINE_LEN_MAX + 1);
1✔
793
        let heap = VString::new(&overflow);
1✔
794
        assert!(
1✔
795
            !heap.is_inline(),
1✔
796
            "length {} should force heap allocation",
797
            overflow.len()
×
798
        );
799
    }
1✔
800

801
    #[test]
802
    fn inline_value_tag_matches() {
1✔
803
        for len in 0..=VString::INLINE_LEN_MAX {
8✔
804
            let input = "z".repeat(len);
8✔
805
            let value = Value::from(input.as_str());
8✔
806
            assert!(value.is_inline_string(), "Value should mark inline string");
8✔
807
            assert_eq!(
8✔
808
                value.ptr_usize() & 0b111,
8✔
809
                TypeTag::InlineString as usize,
8✔
810
                "low bits must store inline string tag"
811
            );
812
            let roundtrip = value.as_string().expect("string value");
8✔
813
            assert_eq!(roundtrip.as_str(), input);
8✔
814
            assert_eq!(roundtrip.as_bytes(), input.as_bytes());
8✔
815
        }
816
    }
1✔
817

818
    #[cfg(target_pointer_width = "64")]
819
    #[test]
820
    fn inline_len_max_is_seven_on_64_bit() {
1✔
821
        assert_eq!(VString::INLINE_LEN_MAX, 7);
1✔
822
    }
1✔
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() {
1✔
834
        let s = VSafeString::new("hello");
1✔
835
        assert_eq!(s.as_str(), "hello");
1✔
836
        assert_eq!(s.len(), 5);
1✔
837
        assert!(!s.is_empty());
1✔
838
    }
1✔
839

840
    #[test]
841
    fn test_safe_string_roundtrip() {
1✔
842
        let original = "<b>bold</b>";
1✔
843
        let safe = VSafeString::new(original);
1✔
844
        assert_eq!(safe.as_str(), original);
1✔
845
    }
1✔
846

847
    #[test]
848
    fn test_safe_string_is_always_heap() {
1✔
849
        // Even short strings should be heap-allocated for safe strings
850
        let short = VSafeString::new("hi");
1✔
851
        assert_eq!(short.len(), 2);
1✔
852
        assert_eq!(short.as_str(), "hi");
1✔
853
        // The value should have tag 1 (StringOrNull) not tag 6 (InlineString)
854
        let value: Value = short.into();
1✔
855
        assert!(!value.is_inline_string());
1✔
856
        assert!(value.is_string());
1✔
857
    }
1✔
858

859
    #[test]
860
    fn test_vstring_is_safe() {
1✔
861
        let normal = VString::new("hello");
1✔
862
        assert!(!normal.is_safe());
1✔
863

864
        let safe = VSafeString::new("hello");
1✔
865
        // When viewed as VString, should still report safe
866
        let as_vstring: VString = safe.into();
1✔
867
        assert!(as_vstring.is_safe());
1✔
868
    }
1✔
869

870
    #[test]
871
    fn test_vstring_into_safe() {
1✔
872
        // Test inline string promotion
873
        let inline = VString::new("hi");
1✔
874
        assert!(inline.is_inline());
1✔
875
        let safe = inline.into_safe();
1✔
876
        assert_eq!(safe.as_str(), "hi");
1✔
877

878
        // Test heap string conversion
879
        let long = "a".repeat(VString::INLINE_LEN_MAX + 10);
1✔
880
        let heap = VString::new(&long);
1✔
881
        assert!(!heap.is_inline());
1✔
882
        let safe_heap = heap.into_safe();
1✔
883
        assert_eq!(safe_heap.as_str(), long);
1✔
884
    }
1✔
885

886
    #[test]
887
    fn test_safe_flag_preserved_through_clone() {
1✔
888
        let safe = VSafeString::new("<b>bold</b>");
1✔
889
        let value: Value = safe.into();
1✔
890
        assert!(value.is_safe_string());
1✔
891

892
        let cloned = value.clone();
1✔
893
        assert!(cloned.is_safe_string());
1✔
894
        assert_eq!(cloned.as_string().unwrap().as_str(), "<b>bold</b>");
1✔
895
    }
1✔
896

897
    #[test]
898
    fn test_value_as_safe_string() {
1✔
899
        let safe = VSafeString::new("safe content");
1✔
900
        let value: Value = safe.into();
1✔
901

902
        // is_string should return true (safe strings ARE strings)
903
        assert!(value.is_string());
1✔
904
        // is_safe_string should also return true
905
        assert!(value.is_safe_string());
1✔
906
        // as_string should work
907
        assert_eq!(value.as_string().unwrap().as_str(), "safe content");
1✔
908
        // as_safe_string should work
909
        assert_eq!(value.as_safe_string().unwrap().as_str(), "safe content");
1✔
910
    }
1✔
911

912
    #[test]
913
    fn test_normal_string_not_safe() {
1✔
914
        let normal = VString::new("normal");
1✔
915
        let value: Value = normal.into();
1✔
916

917
        assert!(value.is_string());
1✔
918
        assert!(!value.is_safe_string());
1✔
919
        assert!(value.as_string().is_some());
1✔
920
        assert!(value.as_safe_string().is_none());
1✔
921
    }
1✔
922

923
    #[test]
924
    fn test_safe_string_equality() {
1✔
925
        let a = VSafeString::new("hello");
1✔
926
        let b = VSafeString::new("hello");
1✔
927
        let c = VSafeString::new("world");
1✔
928

929
        assert_eq!(a, b);
1✔
930
        assert_ne!(a, c);
1✔
931
        assert_eq!(a, "hello");
1✔
932

933
        // Equality with VString
934
        let vstring = VString::new("hello");
1✔
935
        assert_eq!(a, vstring);
1✔
936
        assert_eq!(vstring, a);
1✔
937
    }
1✔
938

939
    #[test]
940
    fn test_safe_string_into_string() {
1✔
941
        let safe = VSafeString::new("test");
1✔
942
        let vstring = safe.into_string();
1✔
943
        assert_eq!(vstring.as_str(), "test");
1✔
944
        assert!(vstring.is_safe()); // Flag should be preserved
1✔
945
    }
1✔
946

947
    #[test]
948
    fn test_safe_flag_constant() {
1✔
949
        // Verify the safe flag uses the high bit
950
        assert_eq!(SAFE_FLAG, 1usize << (usize::BITS - 1));
1✔
951
        // On 64-bit: 0x8000_0000_0000_0000
952
        // On 32-bit: 0x8000_0000
953
    }
1✔
954

955
    #[test]
956
    fn test_safe_string_long() {
1✔
957
        // Test with a string that would definitely be heap-allocated anyway
958
        let long = "a".repeat(1000);
1✔
959
        let safe = VSafeString::new(&long);
1✔
960
        assert_eq!(safe.len(), 1000);
1✔
961
        assert_eq!(safe.as_str(), long);
1✔
962

963
        let value: Value = safe.into();
1✔
964
        assert!(value.is_safe_string());
1✔
965
        assert_eq!(value.as_string().unwrap().len(), 1000);
1✔
966
    }
1✔
967
}
968

969
#[cfg(all(test, feature = "bolero-inline-tests"))]
970
mod 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
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc