#[derive(Debug, Default)]
pub(crate) struct Utf8Stream {
pending: Vec<u8>,
}
impl Utf8Stream {
pub(crate) 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(text) => {
out.push_str(text);
self.pending.clear();
return out;
}
Err(error) => {
let valid = error.valid_up_to();
out.push_str(std::str::from_utf8(&self.pending[..valid]).expect("validated"));
match error.error_len() {
None => {
self.pending.drain(..valid);
return out;
}
Some(bad) => {
out.push(char::REPLACEMENT_CHARACTER);
self.pending.drain(..valid + bad);
}
}
}
}
}
}
pub(crate) fn flush(&mut self) -> String {
if self.pending.is_empty() {
return String::new();
}
self.pending.clear();
char::REPLACEMENT_CHARACTER.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_character_split_across_two_tokens_survives() {
let smiley = "π".as_bytes();
let (first, second) = smiley.split_at(2);
let mut stream = Utf8Stream::default();
assert_eq!(stream.push(first), "", "half a character is not text yet");
assert_eq!(stream.push(second), "π");
assert_eq!(stream.flush(), "", "nothing left over");
}
#[test]
fn one_byte_at_a_time_still_assembles() {
let mut stream = Utf8Stream::default();
let mut out = String::new();
for byte in "δΈη".as_bytes() {
out.push_str(&stream.push(&[*byte]));
}
assert_eq!(out, "δΈη");
}
#[test]
fn ascii_is_emitted_immediately_and_not_held() {
let mut stream = Utf8Stream::default();
assert_eq!(stream.push(b"today? "), "today? ");
assert_eq!(stream.push(&"π".as_bytes()[..1]), "");
assert_eq!(stream.push(&"π".as_bytes()[1..]), "π");
}
#[test]
fn genuinely_invalid_bytes_become_one_replacement_and_do_not_stall() {
let mut stream = Utf8Stream::default();
assert_eq!(stream.push(&[0xff, b'o', b'k']), "\u{fffd}ok");
}
#[test]
fn a_truncated_character_surfaces_at_flush() {
let mut stream = Utf8Stream::default();
assert_eq!(stream.push(&"π".as_bytes()[..3]), "");
assert_eq!(stream.flush(), "\u{fffd}");
assert_eq!(stream.flush(), "", "flush does not repeat itself");
}
}