use std::io::{self, Write};
pub(crate) struct ConsoleSafe<W: Write> {
inner: W,
pending: Vec<u8>,
}
impl<W: Write> ConsoleSafe<W> {
pub(crate) fn new(inner: W) -> Self {
Self {
inner,
pending: Vec::new(),
}
}
fn drain(&mut self, last: bool) -> io::Result<()> {
let mut out = Vec::with_capacity(self.pending.len());
let mut rest: &[u8] = &self.pending;
loop {
match std::str::from_utf8(rest) {
Ok(text) => {
out.extend_from_slice(text.as_bytes());
rest = &[];
break;
}
Err(error) => {
let (valid, after) = rest.split_at(error.valid_up_to());
out.extend_from_slice(valid);
let invalid = match error.error_len() {
Some(length) => length,
None if !last => {
rest = after;
break;
}
None => after.len(),
};
for byte in &after[..invalid] {
write!(out, "\\x{byte:02x}")?;
}
rest = &after[invalid..];
}
}
}
let kept = rest.len();
self.inner.write_all(&out)?;
let start = self.pending.len() - kept;
self.pending.drain(..start);
Ok(())
}
}
impl<W: Write> Write for ConsoleSafe<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.pending.extend_from_slice(buf);
self.drain(false)?;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.drain(true)?;
self.inner.flush()
}
}
impl<W: Write> Drop for ConsoleSafe<W> {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn through(writes: &[&[u8]]) -> Vec<u8> {
let mut out = Vec::new();
{
let mut console = ConsoleSafe::new(&mut out);
for bytes in writes {
console.write_all(bytes).expect("write");
}
console.flush().expect("flush");
}
out
}
#[test]
fn utf8_passes_and_other_bytes_are_escaped() {
assert_eq!(through(&["café".as_bytes()]), "café".as_bytes());
assert_eq!(through(&[b"caf\xe9.c 1\n"]), b"caf\\xe9.c 1\n");
assert_eq!(through(&[b"\xff\xfe"]), b"\\xff\\xfe");
}
#[test]
fn a_sequence_cut_between_writes_is_kept_whole() {
let e_acute = "é".as_bytes();
assert_eq!(
through(&[b"caf", &e_acute[..1], &e_acute[1..], b"\n"]),
"café\n".as_bytes()
);
assert_eq!(through(&[b"caf", &e_acute[..1]]), b"caf\\xc3");
}
}