Skip to main content

gproxy_transform/transform/common/
utf8.rs

1//! Incremental UTF-8 decoding for byte streams.
2//!
3//! Network chunks may split a multi-byte UTF-8 sequence across chunk
4//! boundaries. Decoding each chunk independently (e.g. with
5//! `String::from_utf8_lossy`) turns the split character into U+FFFD
6//! replacement characters. This decoder holds back an incomplete trailing
7//! sequence until the next chunk completes it.
8
9/// Streaming UTF-8 decoder: feed byte chunks, get valid `&str` pushed into a
10/// `String`. Truly invalid bytes are replaced with U+FFFD; an *incomplete*
11/// trailing sequence is buffered until more bytes arrive (or [`Self::flush`]).
12#[derive(Debug, Default)]
13pub struct Utf8StreamDecoder {
14    /// Incomplete trailing UTF-8 sequence (at most 3 bytes) held back until
15    /// the next chunk.
16    pending: Vec<u8>,
17}
18
19impl Utf8StreamDecoder {
20    /// Decode `chunk` (prefixed by any held-back bytes) into `out`.
21    pub fn decode_into(&mut self, chunk: &[u8], out: &mut String) {
22        let owned;
23        let mut rest: &[u8] = if self.pending.is_empty() {
24            chunk
25        } else {
26            self.pending.extend_from_slice(chunk);
27            owned = std::mem::take(&mut self.pending);
28            &owned
29        };
30        loop {
31            match std::str::from_utf8(rest) {
32                Ok(s) => {
33                    out.push_str(s);
34                    return;
35                }
36                Err(e) => {
37                    let (valid, after) = rest.split_at(e.valid_up_to());
38                    out.push_str(std::str::from_utf8(valid).expect("valid prefix"));
39                    match e.error_len() {
40                        // Genuinely invalid bytes: replace and continue.
41                        Some(n) => {
42                            out.push(char::REPLACEMENT_CHARACTER);
43                            rest = &after[n..];
44                        }
45                        // Incomplete trailing sequence: hold back for the
46                        // next chunk.
47                        None => {
48                            self.pending = after.to_vec();
49                            return;
50                        }
51                    }
52                }
53            }
54        }
55    }
56
57    /// End of stream: replace any held-back incomplete sequence with U+FFFD.
58    pub fn flush(&mut self, out: &mut String) {
59        if !self.pending.is_empty() {
60            self.pending.clear();
61            out.push(char::REPLACEMENT_CHARACTER);
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn multibyte_char_split_across_chunks() {
72        let bytes = "汉字🚀".as_bytes(); // 3+3+4 bytes
73        let mut d = Utf8StreamDecoder::default();
74        let mut out = String::new();
75        // Split inside every character.
76        for b in bytes {
77            d.decode_into(std::slice::from_ref(b), &mut out);
78        }
79        d.flush(&mut out);
80        assert_eq!(out, "汉字🚀");
81    }
82
83    #[test]
84    fn invalid_bytes_replaced_incomplete_tail_flushed() {
85        let mut d = Utf8StreamDecoder::default();
86        let mut out = String::new();
87        d.decode_into(b"a\xffb\xe6\xb1", &mut out); // 0xff invalid, e6 b1 incomplete
88        assert_eq!(out, "a\u{fffd}b");
89        d.flush(&mut out);
90        assert_eq!(out, "a\u{fffd}b\u{fffd}");
91    }
92}