json-escape 0.3.1

A no_std, zero-copy, allocation-free library for streaming JSON string escaping and unescaping. Ergonomic, fast, RFC 8259 compliant, with layered APIs for iterators, I/O streaming, and low-level tokens.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
//! More explicit and fine-grained iterators for JSON escaping and unescaping.
//!
//! This module provides an alternative API to the one in the crate root. While the
//! root API yields slices (`&str` or `&[u8]`) that represent the final output,
//! this module's iterators yield "chunk" structs. These structs distinguish between
//! parts of the input that were processed literally and the specific characters
//! that were escaped or unescaped.
//!
//! This approach offers several advantages:
//! - **Greater Control**: You can inspect each component of the transformation,
//!   which can be useful for debugging, logging, or more complex data processing.
//! - **Potential Performance**: By avoiding the need to look up single-byte escape
//!   sequences in a table on every iteration, some workflows may see a minor
//!   performance improvement.
//! - **Clarity**: The structure of the output more closely reflects the transformation
//!   process, which can make the logic easier to follow.
//!
//! # Example: Escaping
//!
//! ```
//! use json_escape::explicit::escape_str;
//!
//! let mut escaper = escape_str("a\nb");
//!
//! // The first chunk contains the literal "a" and the escaped newline.
//! let chunk1 = escaper.next().unwrap();
//! assert_eq!("a", chunk1.literal());
//! assert_eq!(Some(r#"\n"#), chunk1.escaped());
//!
//! // The second chunk contains the literal "b" and no escaped sequence.
//! let chunk2 = escaper.next().unwrap();
//! assert_eq!("b", chunk2.literal());
//! assert_eq!(None, chunk2.escaped());
//!
//! // The iterator is now exhausted.
//! assert!(escaper.next().is_none());
//! ```
//!
//! # Example: Unescaping
//!
//! ```
//! use json_escape::explicit::unescape;
//!
//! let mut unescaper = unescape(br"hello\tworld");
//!
//! // The first chunk contains the literal "hello" and the unescaped tab.
//! let chunk1 = unescaper.next().unwrap().unwrap();
//! assert_eq!(b"hello", chunk1.literal());
//! assert_eq!(Some('\t'), chunk1.unescaped());
//!
//! // The second chunk contains the literal "world" and no unescaped character.
//! let chunk2 = unescaper.next().unwrap().unwrap();
//! assert_eq!(b"world", chunk2.literal());
//! assert_eq!(None, chunk2.unescaped());
//!
//! // The iterator is now exhausted.
//! assert!(unescaper.next().is_none());
//! ```
//!
//! Both `Escape` and `Unescape` iterators provide `display` helpers for easy integration
//! with Rust's formatting system, preserving the zero-allocation benefits of the main API.

#[cfg(feature = "alloc")]
use crate::DecodeUtf8Error;
use crate::token::{EscapeTokens, UnescapeTokens};
use crate::{UnescapeError, display_bytes_utf8};
use core::fmt;
use core::iter::FusedIterator;
use core::str;

#[cfg(feature = "alloc")]
use alloc::{borrow::Cow, string::String, vec::Vec};

//==============================================================================
// Escaping
//==============================================================================

/// Creates an iterator that yields chunks of an escaped JSON string.
///
/// See the [module-level documentation](self) for more details.
#[inline]
pub fn escape_str(s: &str) -> Escape<'_> {
    Escape {
        bytes: s.as_bytes(),
    }
}

/// A chunk of a JSON-escaped string, separating the literal part from the escaped sequence.
///
/// This struct is yielded by the [`Escape`] iterator.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EscapedChunk<'a> {
    /// A slice of the original input that did not require escaping.
    literal: &'a str,
    /// The escaped sequence (e.g., `r#"\n"#`, `r#"\""#`) that immediately follows the literal part.
    /// Is `None` if this is the last chunk and it has no trailing escape.
    escaped: Option<&'static str>,
}

impl<'a> EscapedChunk<'a> {
    /// Returns the literal part of the chunk, which is a slice of the original string.
    #[inline]
    pub const fn literal(&self) -> &'a str {
        self.literal
    }

    /// Returns the escaped part of the chunk, if any.
    #[inline]
    pub const fn escaped(&self) -> Option<&'static str> {
        self.escaped
    }
}

impl<'a> fmt::Display for EscapedChunk<'a> {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.literal)?;
        if let Some(s) = self.escaped {
            f.write_str(s)?;
        }
        Ok(())
    }
}

/// An iterator over a string that yields [`EscapedChunk`]s.
///
/// Created by the [`escape_str`] function.
#[derive(Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct Escape<'a> {
    pub(crate) bytes: &'a [u8],
}

impl<'a> Iterator for Escape<'a> {
    type Item = EscapedChunk<'a>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        if self.bytes.is_empty() {
            return None;
        }

        // SAFETY: Input is string
        let (literal, rest) = unsafe { EscapeTokens::split_at_escape(self.bytes) };

        Some(EscapedChunk {
            literal,
            escaped: {
                if rest.is_empty() {
                    self.bytes = rest;
                    None
                } else {
                    // An escapable byte is at the beginning of the slice.
                    self.bytes = &rest[1..];
                    Some(
                        EscapeTokens::escape(rest[0])
                            .expect("find_escape_char found a byte not in ESCAPE_TABLE"),
                    )
                }
            },
        })
    }

    // TODO: size_hint
}

impl<'a> FusedIterator for Escape<'a> {}

impl<'a> fmt::Display for Escape<'a> {
    /// This allows the escaped output to be written directly to a formatter
    /// without intermediate allocation.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for chunk in self.clone() {
            write!(f, "{chunk}")?;
        }
        Ok(())
    }
}

impl fmt::Debug for Escape<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Escape").finish_non_exhaustive()
    }
}

impl<B: AsRef<[u8]> + ?Sized> PartialEq<B> for Escape<'_> {
    /// Compares the escaped output with any byte-slice-like object.
    ///
    /// This is a convenience for testing, allowing you to check the fully
    /// concatenated result of an `Escape` iterator against a known `&str` or `&[u8]`.
    fn eq(&self, other: &B) -> bool {
        let mut other = other.as_ref();
        for chunk in self.clone() {
            // Check literal part
            if !other.starts_with(chunk.literal.as_bytes()) {
                return false;
            }
            other = &other[chunk.literal.len()..];

            // Check escaped part
            if let Some(escaped_str) = chunk.escaped {
                if !other.starts_with(escaped_str.as_bytes()) {
                    return false;
                }
                other = &other[escaped_str.len()..];
            }
        }
        other.is_empty()
    }
}

impl<'a, 'b> PartialEq<Escape<'a>> for Escape<'b> {
    /// Compares two `Escape` iterators for equality.
    ///
    /// Two `Escape` iterators are considered equal if they'll produce the same **output**.
    /// It first performs a fast check on the underlying byte slices.
    fn eq(&self, other: &Escape<'a>) -> bool {
        // The crate parallel is easier
        crate::Escape {
            inner: EscapeTokens { bytes: self.bytes },
        } == crate::Escape {
            inner: EscapeTokens { bytes: other.bytes },
        }
    }
}

#[cfg(feature = "alloc")]
impl<'a> From<Escape<'a>> for Cow<'a, str> {
    /// Efficiently collects the escaped parts into a `Cow<'a, str>`.
    ///
    /// This implementation is optimized to avoid allocation if possible:
    /// - If the input string requires **no escaping**, it returns `Cow::Borrowed`
    ///   with a slice of the original string.
    /// - If escaping is needed, it allocates a `String` and returns `Cow::Owned`.
    fn from(mut iter: Escape<'a>) -> Self {
        match iter.next() {
            None => Cow::Borrowed(""),
            Some(first) => {
                if first.escaped.is_none() {
                    // No escape in the first (and only) chunk, so no escaping was needed.
                    Cow::Borrowed(first.literal)
                } else {
                    // Escaping occurred. We must allocate.
                    let mut s = String::with_capacity(
                        first.literal.len() + first.escaped.unwrap().len() + iter.bytes.len(),
                    );
                    s.push_str(first.literal);
                    s.push_str(first.escaped.unwrap());
                    s.extend(iter);
                    Cow::Owned(s)
                }
            }
        }
    }
}

//==============================================================================
// Unescaping
//==============================================================================

/// Creates an iterator that yields chunks of an unescaped JSON string.
///
/// See the [module-level documentation](self) for more details.
#[inline]
pub fn unescape<I: AsRef<[u8]> + ?Sized>(input: &I) -> Unescape<'_> {
    Unescape {
        bytes: input.as_ref(),
    }
}

/// Creates a streaming JSON string unescaper that handles enclosing quotes.
///
/// This function is a convenience wrapper around [`unescape`]. If the input byte
/// slice starts and ends with a double-quote (`"`), the quotes are trimmed
/// before the content is unescaped.
///
/// If the input is not enclosed in quotes, this function behaves identically to
/// [`unescape`].
///
/// # Examples
///
/// ```
/// use json_escape::explicit::unescape_quoted;
///
/// // An input string with quotes and an escaped tab.
/// let bytes = br#""\tline""#;
/// let mut unescaper = unescape_quoted(bytes);
///
/// // The first chunk is the unescaped tab character.
/// let chunk1 = unescaper.next().unwrap().unwrap();
/// assert_eq!(b"", chunk1.literal());
/// assert_eq!(Some('\t'), chunk1.unescaped());
///
/// // The second chunk is the literal "line".
/// let chunk2 = unescaper.next().unwrap().unwrap();
/// assert_eq!(b"line", chunk2.literal());
/// assert_eq!(None, chunk2.unescaped());
///
/// // The iterator is now exhausted.
/// assert!(unescaper.next().is_none());
/// ```
#[inline]
pub fn unescape_quoted(bytes: &[u8]) -> Unescape<'_> {
    let inner = if bytes.len() >= 2 && bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"') {
        &bytes[1..bytes.len() - 1]
    } else {
        bytes
    };
    unescape(inner)
}

/// A chunk of a JSON-unescaped byte slice, separating the literal part from the unescaped character.
///
/// This struct is yielded by the [`Unescape`] iterator.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnescapedChunk<'a> {
    /// A slice of the original input that did not require unescaping.
    pub(crate) literal: &'a [u8],
    /// The single character that was unescaped.
    /// Is `None` if this is the last chunk and it has no trailing unescaped character.
    pub(crate) unescaped: Option<char>,
}

impl<'a> UnescapedChunk<'a> {
    /// Returns the literal part of the chunk, which is a slice of the original bytes.
    #[inline]
    pub const fn literal(&self) -> &'a [u8] {
        self.literal
    }

    /// Returns the unescaped character, if any.
    #[inline]
    pub const fn unescaped(&self) -> Option<char> {
        self.unescaped
    }

    /// Returns a displayable wrapper that will format the chunk as a UTF-8 string.
    ///
    /// If the literal part of the chunk contains invalid UTF-8 sequences, this
    /// will result in a `fmt::Error`.
    pub fn display_utf8(&self) -> DisplayUnescapedChunk<'_> {
        DisplayUnescapedChunk {
            chunk: self,
            lossy: false,
        }
    }

    /// Returns a displayable wrapper that will format the chunk as a lossy UTF-8 string.
    ///
    /// Any invalid UTF-8 sequences in the literal part of the chunk will be
    /// replaced with the U+FFFD replacement character.
    pub fn display_utf8_lossy(&self) -> DisplayUnescapedChunk<'_> {
        DisplayUnescapedChunk {
            chunk: self,
            lossy: true,
        }
    }
}

/// Helper struct for safely displaying an [`UnescapedChunk`].
pub struct DisplayUnescapedChunk<'a> {
    chunk: &'a UnescapedChunk<'a>,
    lossy: bool,
}

impl<'a> fmt::Display for DisplayUnescapedChunk<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        display_bytes_utf8(self.chunk.literal, f, self.lossy)?;
        if let Some(c) = self.chunk.unescaped {
            use fmt::Write as _;

            f.write_char(c)?;
        }
        Ok(())
    }
}

/// An iterator over a byte slice that yields [`UnescapedChunk`]s.
///
/// Created by the [`unescape`] function.
#[derive(Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct Unescape<'a> {
    pub(crate) bytes: &'a [u8],
}

impl<'a> Unescape<'a> {
    /// Decodes the unescaped byte stream into a UTF-8 string.
    ///
    /// This method consumes the iterator and collects all resulting byte chunks
    /// into a `Cow<[u8]>`, which is then validated as UTF-8. If an unescaping
    /// error occurs, it's returned immediately. If the final sequence of bytes
    /// is not valid UTF-8, a UTF-8 error is returned.
    ///
    /// This is optimized to return a `Cow::Borrowed` if no escapes were present
    /// in the input, avoiding allocation.
    ///
    /// **Requires the `alloc` feature.**
    ///
    /// # Example
    ///
    /// ```
    /// # #[cfg(feature = "alloc")] {
    /// use json_escape::explicit::unescape;
    ///
    /// let input = r#"Emoji: \uD83D\uDE00"#;
    /// let cow = unescape(input).decode_utf8().unwrap();
    ///
    /// assert_eq!(cow, "Emoji: 😀");
    /// # }
    /// ```
    #[cfg(feature = "alloc")]
    pub fn decode_utf8(self) -> Result<Cow<'a, str>, DecodeUtf8Error> {
        match self.try_into().map_err(DecodeUtf8Error::Unescape)? {
            Cow::Borrowed(bytes) => str::from_utf8(bytes)
                .map(Cow::Borrowed)
                .map_err(DecodeUtf8Error::Utf8),
            Cow::Owned(bytes) => String::from_utf8(bytes)
                .map(Cow::Owned)
                .map_err(|e| DecodeUtf8Error::Utf8(e.utf8_error())),
        }
    }

    /// Decodes the unescaped byte stream lossily into a UTF-8 string.
    ///
    /// This is similar to [`Unescape::decode_utf8`] but replaces any invalid UTF-8 sequences
    /// with the replacement character (`U+FFFD`) instead of returning an error.
    ///
    /// An `UnescapeError` can still be returned if the JSON escaping itself is invalid.
    ///
    /// **Requires the `alloc` feature.**
    #[cfg(feature = "alloc")]
    pub fn decode_utf8_lossy(self) -> Result<Cow<'a, str>, UnescapeError> {
        use crate::decode_utf8_lossy;

        Ok(decode_utf8_lossy(self.try_into()?))
    }

    /// Returns a wrapper that implements [`fmt::Display`].
    ///
    /// If an unescaping error or invalid UTF-8 sequence is encountered,
    /// a `fmt::Error` is returned, which will cause `format!` and friends to panic.
    pub fn display_utf8(self) -> DisplayUnescape<'a> {
        DisplayUnescape {
            inner: self,
            lossy: false,
        }
    }

    /// Returns a wrapper that implements [`fmt::Display` for lossy UTF-8 decoding.
    ///
    /// Invalid UTF-8 sequences will be replaced with the replacement character.
    /// An unescaping error will still result in a `fmt::Error`.
    pub fn display_utf8_lossy(self) -> DisplayUnescape<'a> {
        DisplayUnescape {
            inner: self,
            lossy: true,
        }
    }
}

impl<'a> Iterator for Unescape<'a> {
    type Item = Result<UnescapedChunk<'a>, UnescapeError>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        if self.bytes.is_empty() {
            return None;
        }

        let (literal, rest) = UnescapeTokens::split_at_escape(self.bytes);

        Some(Ok(UnescapedChunk {
            literal,
            unescaped: {
                if rest.is_empty() {
                    // there's no remainder, we just have a literal.
                    self.bytes = rest;
                    None
                } else {
                    // rest starts with '\\'
                    let mut remainder = &rest[1..];
                    match UnescapeTokens::handle_escape(&mut remainder) {
                        Ok(unescaped_char) => {
                            self.bytes = remainder;
                            Some(unescaped_char)
                        }
                        Err(err) => return Some(Err(err)),
                    }
                }
            },
        }))
    }

    // TODO: sizehint
}

impl<'a> FusedIterator for Unescape<'a> {}

impl fmt::Debug for Unescape<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Unescape").finish_non_exhaustive()
    }
}

impl<B: AsRef<[u8]> + ?Sized> PartialEq<B> for Unescape<'_> {
    /// Compares the unescaped output with a byte-slice-like object.
    ///
    /// Returns `true` if the iterator successfully unescapes to produce a byte
    /// sequence identical to `other`. If an error occurs, returns `false`.
    fn eq(&self, other: &B) -> bool {
        let mut other = other.as_ref();
        let mut char_buf = [0u8; 4];

        for result in self.clone() {
            match result {
                Ok(chunk) => {
                    // Check literal part
                    if !other.starts_with(chunk.literal) {
                        return false;
                    }
                    other = &other[chunk.literal.len()..];

                    // Check unescaped part
                    if let Some(c) = chunk.unescaped {
                        let char_bytes = c.encode_utf8(&mut char_buf);
                        if !other.starts_with(char_bytes.as_bytes()) {
                            return false;
                        }
                        other = &other[char_bytes.len()..];
                    }
                }
                Err(_) => return false, // An erroring iterator cannot be equal.
            }
        }
        other.is_empty()
    }
}

impl<B: AsRef<[u8]>> PartialEq<Unescape<'_>> for Result<B, UnescapeError> {
    /// Compares the unescaper's outcome with a `Result`.
    ///
    /// This allows for precise testing of `Unescape` against either a
    /// successful outcome (`Ok(bytes)`) or a specific failure (`Err(error)`).
    fn eq(&self, unescape: &Unescape<'_>) -> bool {
        match self {
            Ok(expected_bytes) => unescape == expected_bytes,
            Err(expected_error) => {
                for result in unescape.clone() {
                    if let Err(actual_error) = result {
                        // The iterator's first error is its final outcome.
                        return actual_error == *expected_error;
                    }
                }
                // `unescape` completed successfully, but an error was expected.
                false
            }
        }
    }
}

impl<'a, 'b> PartialEq<Unescape<'a>> for Unescape<'b> {
    /// Compares two `Unescape` iterators for equality based on their terminal result.
    ///
    /// The equality of two `Unescape` iterators is determined by the final `Result`
    /// that would be obtained if each iterator were fully consumed (e.g., by using `try_collect()`).
    ///
    /// The specific rules are as follows:
    ///
    /// 1.  **Error vs. Error**: If both iterators terminate with an `Err`, they are
    ///     considered **equal** if and only if their `UnescapeError`s are identical.
    ///     Any bytes successfully unescaped *before* the error are ignored in this case.
    /// 2.  **Success vs. Success**: If both iterators terminate with `Ok`, they are
    ///     considered **equal** if and only if the complete sequence of unescaped bytes
    ///     is identical for both.
    /// 3.  **Success vs. Error**: If one iterator terminates with `Ok` and the other
    ///     with `Err`, they are always **not equal**.
    ///
    /// # Example
    ///
    /// ```
    /// use json_escape::explicit::unescape;
    ///
    /// // Case 1: Both iterators produce the same error. They are equal,
    /// // even though their valid prefixes ("a" and "b") are different.
    /// let failing_a = unescape(r#"a\k"#);
    /// let failing_b = unescape(r#"b\k"#);
    /// assert_eq!(failing_a, failing_b);
    ///
    /// // Case 2: Both iterators succeed. Equality depends on the byte stream.
    /// let successful_a = unescape(r#"hello\nworld"#);
    /// let successful_b = unescape(r#"hello\nworld"#);
    /// assert_eq!(successful_a, successful_b);
    ///
    /// let successful_c = unescape(r#"different"#);
    /// assert_ne!(successful_a, successful_c);
    ///
    /// // Case 3: One succeeds and one fails. They are not equal.
    /// let succeeding = unescape(r#"stop"#);
    /// let failing = unescape(r#"stop\k"#);
    /// assert_ne!(succeeding, failing);
    ///
    /// // Case 4: Both iterators fail differently. They are not equal.
    /// let failing_a = unescape(r#"data:\k"#);
    /// let failing_b = unescape(r#"data:\"#);
    /// assert_ne!(failing_a, failing_b);
    /// ```
    fn eq(&self, other: &Unescape<'a>) -> bool {
        // The crate parallel is easier
        crate::unescape(self.bytes) == crate::unescape(other.bytes)
    }
}

#[cfg(feature = "alloc")]
impl<'a> TryFrom<Unescape<'a>> for Cow<'a, [u8]> {
    type Error = UnescapeError;

    /// Efficiently collects the unescaped bytes into a `Cow<'a, [u8]>`.
    ///
    /// Returns `Cow::Borrowed` if no escape sequences were present, avoiding
    /// allocation. Otherwise, returns `Cow::Owned`. If an error occurs, it's
    /// returned immediately.
    fn try_from(mut value: Unescape<'a>) -> Result<Self, Self::Error> {
        use crate::token::append_char;

        match value.next() {
            None => Ok(Cow::Borrowed(b"")),
            Some(Ok(first)) => {
                if first.unescaped.is_none() {
                    // The first and only chunk has no unescaped part. No allocation needed.
                    Ok(Cow::Borrowed(first.literal))
                } else {
                    // An escape was processed. Must allocate and collect the rest.
                    let mut buf = Vec::with_capacity(value.bytes.len() + 16);
                    buf.extend_from_slice(first.literal);

                    if let Some(c) = first.unescaped {
                        append_char(&mut buf, c);
                    }

                    for item in value {
                        let chunk = item?;
                        buf.extend_from_slice(chunk.literal);
                        if let Some(c) = chunk.unescaped {
                            append_char(&mut buf, c);
                        }
                    }
                    Ok(Cow::Owned(buf))
                }
            }
            Some(Err(e)) => Err(e),
        }
    }
}

/// A wrapper struct for implementing `fmt::Display` on an [`Unescape`] iterator.
pub struct DisplayUnescape<'a> {
    inner: Unescape<'a>,
    lossy: bool,
}

impl<'a> fmt::Display for DisplayUnescape<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for chunk_result in self.inner.clone() {
            match chunk_result {
                Ok(chunk) => {
                    let display_chunk = DisplayUnescapedChunk {
                        chunk: &chunk,
                        lossy: self.lossy,
                    };
                    write!(f, "{}", display_chunk)?;
                }
                Err(_) => return Err(fmt::Error), // Signal error to formatter
            }
        }
        Ok(())
    }
}

//==============================================================================
// Iterator Trait Implementations
//==============================================================================

#[cfg(feature = "alloc")]
mod iter_traits {
    use crate::token::append_char;

    use super::{EscapedChunk, UnescapedChunk};
    use alloc::string::String;
    use alloc::vec::Vec;

    /// Collects an iterator of escaped chunks into a single `String`.
    impl<'a> FromIterator<EscapedChunk<'a>> for String {
        #[inline]
        fn from_iter<I: IntoIterator<Item = EscapedChunk<'a>>>(iter: I) -> String {
            let mut s = String::new();
            s.extend(iter);
            s
        }
    }

    /// Extends a `String` with an iterator of escaped chunks.
    impl<'a> Extend<EscapedChunk<'a>> for String {
        #[inline]
        fn extend<I: IntoIterator<Item = EscapedChunk<'a>>>(&mut self, iter: I) {
            iter.into_iter().for_each(move |chunk| {
                self.push_str(chunk.literal);
                if let Some(escaped_str) = chunk.escaped {
                    self.push_str(escaped_str);
                }
            });
        }
    }

    /// Collects an iterator of unescaped chunks into a byte vector.
    impl<'a> FromIterator<UnescapedChunk<'a>> for Vec<u8> {
        #[inline]
        fn from_iter<I: IntoIterator<Item = UnescapedChunk<'a>>>(iter: I) -> Vec<u8> {
            let mut buf = Vec::new();
            buf.extend(iter);
            buf
        }
    }

    /// Extends a byte vector with an iterator of unescaped chunks.
    impl<'a> Extend<UnescapedChunk<'a>> for Vec<u8> {
        #[inline]
        fn extend<I: IntoIterator<Item = UnescapedChunk<'a>>>(&mut self, iter: I) {
            iter.into_iter().for_each(move |chunk| {
                self.extend_from_slice(chunk.literal);
                if let Some(c) = chunk.unescaped {
                    append_char(self, c)
                }
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    impl<'a> EscapedChunk<'a> {
        /// Creates a new `EscapedChunk`.
        const fn new(literal: &'a str, escaped: Option<&'static str>) -> Self {
            Self { literal, escaped }
        }
    }

    impl<'a> UnescapedChunk<'a> {
        /// Creates a new `UnescapedChunk`.
        const fn new(literal: &'a [u8], unescaped: Option<char>) -> Self {
            Self { literal, unescaped }
        }
    }

    #[test]
    fn escape_chunks() {
        let mut it = escape_str("a\nb\"c");
        assert_eq!(
            it.next(),
            Some(EscapedChunk::new("a", Some(r#"\n"#))),
            "Chunk 1"
        );
        assert_eq!(
            it.next(),
            Some(EscapedChunk::new("b", Some(r#"\""#))),
            "Chunk 2"
        );
        assert_eq!(it.next(), Some(EscapedChunk::new("c", None)), "Chunk 3");
        assert_eq!(it.next(), None, "End of iterator");
    }

    #[test]
    fn unescape_chunks() {
        let mut it = unescape(br"xy\t\u0020z");
        assert_eq!(
            it.next().unwrap().unwrap(),
            UnescapedChunk::new(b"xy", Some('\t')),
            "Chunk 1"
        );
        assert_eq!(
            it.next().unwrap().unwrap(),
            UnescapedChunk::new(b"", Some(' ')),
            "Chunk 2"
        );
        assert_eq!(
            it.next().unwrap().unwrap(),
            UnescapedChunk::new(b"z", None),
            "Chunk 3"
        );
        assert_eq!(it.next(), None, "End of iterator");
    }

    #[test]
    fn test_escape_against_collected_string() {
        assert_eq!(
            escape_str("Hello, world!").collect::<String>(),
            "Hello, world!"
        );
        assert_eq!(escape_str("a\"b").collect::<String>(), r#"a\"b"#);
        assert_eq!(escape_str("\0").collect::<String>(), r#"\u0000"#);
        assert_eq!(
            escape_str("path/to/file").collect::<String>(),
            r#"path/to/file"#
        );

        escape_str(r#"Unicode test: éàçüö. Emoji: 😀. More symbols: ❤️✅."#).for_each(|_| {});
    }

    #[test]
    fn test_unescape_against_collected_string() {
        assert_eq!(
            unescape(br"Hello, world!").decode_utf8().unwrap(),
            "Hello, world!"
        );
        assert_eq!(unescape(br"a\nb").decode_utf8().unwrap(), "a\nb");
        assert_eq!(unescape(br"\uD83D\uDE00").decode_utf8().unwrap(), "😀");
    }

    #[test]
    fn unescape_error_propagation() {
        let mut it = unescape(br"valid\k");

        // A better design: the error is the *only* thing that comes out for that step.
        // The current implementation bundles the literal with the result of the escape.
        // Let's stick with that.
        let first_chunk = it.next().unwrap();
        assert!(matches!(first_chunk, Err(UnescapeError { .. })));
    }

    // Inspired by and copied from memchr
    #[test]
    fn sync_regression() {
        use core::panic::{RefUnwindSafe, UnwindSafe};

        fn assert_send_sync<T: Send + Sync + UnwindSafe + RefUnwindSafe>() {}
        assert_send_sync::<Unescape<'_>>();
        assert_send_sync::<Escape<'_>>();

        assert_send_sync::<UnescapedChunk<'_>>();
        assert_send_sync::<EscapedChunk<'_>>();
    }
}