#[derive(Debug, Default)]
pub struct Utf8StreamDecoder {
pending: Vec<u8>,
}
impl Utf8StreamDecoder {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, bytes: &[u8]) -> String {
self.pending.extend_from_slice(bytes);
let mut out = String::new();
loop {
match std::str::from_utf8(&self.pending) {
Ok(s) => {
out.push_str(s);
self.pending.clear();
break;
}
Err(e) => {
let valid = e.valid_up_to();
out.push_str(std::str::from_utf8(&self.pending[..valid]).unwrap());
match e.error_len() {
Some(n) => {
out.push('\u{FFFD}');
self.pending.drain(..valid + n);
}
None => {
self.pending.drain(..valid);
break;
}
}
}
}
}
out
}
pub fn flush(&mut self) -> String {
let out = String::from_utf8_lossy(&self.pending).into_owned();
self.pending.clear();
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn passes_ascii_through() {
let mut d = Utf8StreamDecoder::new();
assert_eq!(d.push(b"hello"), "hello");
assert_eq!(d.flush(), "");
}
#[test]
fn reassembles_multibyte_char_split_across_chunks() {
let mut d = Utf8StreamDecoder::new();
let euro = "€".as_bytes(); assert_eq!(d.push(&euro[..1]), "");
assert_eq!(d.push(&euro[1..2]), "");
assert_eq!(d.push(&euro[2..]), "€");
}
#[test]
fn mixed_text_and_split_emoji() {
let mut d = Utf8StreamDecoder::new();
let s = "ok 🚀 done".as_bytes();
let cut = 5; let first = d.push(&s[..cut]);
let second = d.push(&s[cut..]);
assert_eq!(format!("{first}{second}"), "ok 🚀 done");
}
#[test]
fn replaces_genuinely_invalid_bytes() {
let mut d = Utf8StreamDecoder::new();
assert_eq!(d.push(&[b'a', 0xFF, b'b']), "a\u{FFFD}b");
}
#[test]
fn flush_lossily_drains_incomplete_tail() {
let mut d = Utf8StreamDecoder::new();
assert_eq!(d.push("€".as_bytes()[..2].as_ref()), "");
assert_eq!(d.flush(), "\u{FFFD}");
}
}