Coverage Report

Created: 2026-10-28 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/utf8-zero-0.8.1/src/lib.rs
Line
Count
Source
1
#![no_std]
2
#![deny(missing_docs)]
3
4
//! Zero-copy, incremental UTF-8 decoding with error handling.
5
//!
6
//! Three levels of API:
7
//!
8
//! * [`decode()`] -- low-level, single-shot decode of a byte slice. Returns the valid
9
//!   prefix and either an invalid sequence or an incomplete suffix that can be completed
10
//!   with more input.
11
//! * [`LossyDecoder`] -- a push-based streaming decoder. Feed it chunks of bytes and it
12
//!   calls back with `&str` slices, replacing errors with U+FFFD.
13
//! * [`BufReadDecoder`] (requires the `std` feature) -- a pull-based streaming decoder
14
//!   wrapping any [`std::io::BufRead`], with both strict and lossy modes.
15
16
#[cfg(feature = "std")]
17
extern crate std;
18
19
mod lossy;
20
#[cfg(feature = "std")]
21
mod read;
22
23
pub use lossy::LossyDecoder;
24
#[cfg(feature = "std")]
25
pub use read::{BufReadDecoder, BufReadDecoderError};
26
27
use core::cmp;
28
use core::fmt;
29
use core::str;
30
31
/// The replacement character, U+FFFD. In lossy decoding, insert it for every decoding error.
32
pub const REPLACEMENT_CHARACTER: &str = "\u{FFFD}";
33
34
/// Error from [`decode()`] when the input is not entirely valid UTF-8.
35
#[derive(Debug, Copy, Clone)]
36
pub enum DecodeError<'a> {
37
    /// In lossy decoding insert `valid_prefix`, then `"\u{FFFD}"`,
38
    /// then call `decode()` again with `remaining_input`.
39
    Invalid {
40
        /// The leading valid UTF-8 portion of the input.
41
        valid_prefix: &'a str,
42
        /// The bytes that form the invalid sequence.
43
        invalid_sequence: &'a [u8],
44
        /// The bytes after the invalid sequence, not yet decoded.
45
        remaining_input: &'a [u8],
46
    },
47
48
    /// Call the `incomplete_suffix.try_complete` method with more input when available.
49
    /// If no more input is available, this is an invalid byte sequence.
50
    Incomplete {
51
        /// The leading valid UTF-8 portion of the input.
52
        valid_prefix: &'a str,
53
        /// The trailing bytes that start a multi-byte code point but are not complete.
54
        incomplete_suffix: Incomplete,
55
    },
56
}
57
58
impl<'a> fmt::Display for DecodeError<'a> {
59
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60
0
        match *self {
61
            DecodeError::Invalid {
62
0
                valid_prefix,
63
0
                invalid_sequence,
64
0
                remaining_input,
65
0
            } => write!(
66
0
                f,
67
0
                "found invalid byte sequence {invalid_sequence:02x?} after \
68
0
                 {valid_byte_count} valid bytes, followed by {unprocessed_byte_count} more \
69
0
                 unprocessed bytes",
70
                invalid_sequence = invalid_sequence,
71
0
                valid_byte_count = valid_prefix.len(),
72
0
                unprocessed_byte_count = remaining_input.len()
73
            ),
74
            DecodeError::Incomplete {
75
0
                valid_prefix,
76
0
                incomplete_suffix,
77
0
            } => write!(
78
0
                f,
79
0
                "found incomplete byte sequence {incomplete_suffix:02x?} after \
80
0
                 {valid_byte_count} bytes",
81
                incomplete_suffix = incomplete_suffix,
82
0
                valid_byte_count = valid_prefix.len()
83
            ),
84
        }
85
0
    }
86
}
87
88
#[cfg(feature = "std")]
89
impl<'a> std::error::Error for DecodeError<'a> {}
90
91
/// An incomplete byte sequence for a multi-byte UTF-8 code point.
92
///
93
/// Feed more bytes via [`try_complete()`](Incomplete::try_complete) to finish decoding.
94
#[derive(Debug, Copy, Clone)]
95
pub struct Incomplete {
96
    /// Internal buffer holding the incomplete bytes (up to 4).
97
    pub buffer: [u8; 4],
98
    /// How many bytes in `buffer` are occupied.
99
    pub buffer_len: u8,
100
}
101
102
/// Decode a byte slice as UTF-8, returning the valid prefix on error.
103
///
104
/// Unlike [`std::str::from_utf8()`], this distinguishes between invalid and
105
/// incomplete byte sequences so that callers can request more input.
106
///
107
/// ```
108
/// use utf8_zero::{decode, DecodeError};
109
///
110
/// // Fully valid input.
111
/// assert_eq!(decode(b"hello").unwrap(), "hello");
112
///
113
/// // Invalid byte — returns the valid prefix and the bad sequence.
114
/// match decode(b"hello\xC0world") {
115
///     Err(DecodeError::Invalid { valid_prefix, .. }) => {
116
///         assert_eq!(valid_prefix, "hello");
117
///     }
118
///     _ => unreachable!(),
119
/// }
120
///
121
/// // Input ends mid-codepoint — returns Incomplete so the caller can
122
/// // supply more bytes.
123
/// match decode(b"\xC3") {
124
///     Err(DecodeError::Incomplete { valid_prefix, incomplete_suffix }) => {
125
///         assert_eq!(valid_prefix, "");
126
///         assert_eq!(incomplete_suffix.buffer_len, 1);
127
///     }
128
///     _ => unreachable!(),
129
/// }
130
/// ```
131
0
pub fn decode(input: &[u8]) -> Result<&str, DecodeError<'_>> {
132
0
    let error = match str::from_utf8(input) {
133
0
        Ok(valid) => return Ok(valid),
134
0
        Err(error) => error,
135
    };
136
137
    // FIXME: separate function from here to guide inlining?
138
0
    let (valid, after_valid) = input.split_at(error.valid_up_to());
139
0
    let valid = unsafe { str::from_utf8_unchecked(valid) };
140
141
0
    match error.error_len() {
142
0
        Some(invalid_sequence_length) => {
143
0
            let (invalid, rest) = after_valid.split_at(invalid_sequence_length);
144
0
            Err(DecodeError::Invalid {
145
0
                valid_prefix: valid,
146
0
                invalid_sequence: invalid,
147
0
                remaining_input: rest,
148
0
            })
149
        }
150
0
        None => Err(DecodeError::Incomplete {
151
0
            valid_prefix: valid,
152
0
            incomplete_suffix: Incomplete::new(after_valid),
153
0
        }),
154
    }
155
0
}
156
157
impl Incomplete {
158
    /// Create an empty `Incomplete` with no buffered bytes.
159
0
    pub fn empty() -> Self {
160
0
        Incomplete {
161
0
            buffer: [0, 0, 0, 0],
162
0
            buffer_len: 0,
163
0
        }
164
0
    }
165
166
    /// Returns `true` if no bytes are buffered.
167
0
    pub fn is_empty(&self) -> bool {
168
0
        self.buffer_len == 0
169
0
    }
170
171
    /// Create an `Incomplete` pre-filled with the given bytes.
172
0
    pub fn new(bytes: &[u8]) -> Self {
173
0
        let mut buffer = [0, 0, 0, 0];
174
0
        let len = bytes.len();
175
0
        buffer[..len].copy_from_slice(bytes);
176
0
        Incomplete {
177
0
            buffer,
178
0
            buffer_len: len as u8,
179
0
        }
180
0
    }
181
182
    /// * `None`: still incomplete, call `try_complete` again with more input.
183
    ///   If no more input is available, this is invalid byte sequence.
184
    /// * `Some((result, remaining_input))`: We’re done with this `Incomplete`.
185
    ///   To keep decoding, pass `remaining_input` to `decode()`.
186
    #[allow(clippy::type_complexity)]
187
0
    pub fn try_complete<'input>(
188
0
        &mut self,
189
0
        input: &'input [u8],
190
0
    ) -> Option<(Result<&str, &[u8]>, &'input [u8])> {
191
0
        let (consumed, opt_result) = self.try_complete_offsets(input);
192
0
        let result = opt_result?;
193
0
        let remaining_input = &input[consumed..];
194
0
        let result_bytes = self.take_buffer();
195
0
        let result = match result {
196
0
            Ok(()) => Ok(unsafe { str::from_utf8_unchecked(result_bytes) }),
197
0
            Err(()) => Err(result_bytes),
198
        };
199
0
        Some((result, remaining_input))
200
0
    }
201
202
0
    fn take_buffer(&mut self) -> &[u8] {
203
0
        let len = self.buffer_len as usize;
204
0
        self.buffer_len = 0;
205
0
        &self.buffer[..len]
206
0
    }
207
208
    /// (consumed_from_input, None): not enough input
209
    /// (consumed_from_input, Some(Err(()))): error bytes in buffer
210
    /// (consumed_from_input, Some(Ok(()))): UTF-8 string in buffer
211
0
    fn try_complete_offsets(&mut self, input: &[u8]) -> (usize, Option<Result<(), ()>>) {
212
0
        let initial_buffer_len = self.buffer_len as usize;
213
        let copied_from_input;
214
0
        {
215
0
            let unwritten = &mut self.buffer[initial_buffer_len..];
216
0
            copied_from_input = cmp::min(unwritten.len(), input.len());
217
0
            unwritten[..copied_from_input].copy_from_slice(&input[..copied_from_input]);
218
0
        }
219
0
        let spliced = &self.buffer[..initial_buffer_len + copied_from_input];
220
0
        match str::from_utf8(spliced) {
221
            Ok(_) => {
222
0
                self.buffer_len = spliced.len() as u8;
223
0
                (copied_from_input, Some(Ok(())))
224
            }
225
0
            Err(error) => {
226
0
                let valid_up_to = error.valid_up_to();
227
0
                if valid_up_to > 0 {
228
0
                    let consumed = valid_up_to.checked_sub(initial_buffer_len).unwrap();
229
0
                    self.buffer_len = valid_up_to as u8;
230
0
                    (consumed, Some(Ok(())))
231
                } else {
232
0
                    match error.error_len() {
233
0
                        Some(invalid_sequence_length) => {
234
0
                            let consumed = invalid_sequence_length
235
0
                                .checked_sub(initial_buffer_len)
236
0
                                .unwrap();
237
0
                            self.buffer_len = invalid_sequence_length as u8;
238
0
                            (consumed, Some(Err(())))
239
                        }
240
                        None => {
241
0
                            self.buffer_len = spliced.len() as u8;
242
0
                            (copied_from_input, None)
243
                        }
244
                    }
245
                }
246
            }
247
        }
248
0
    }
249
}