const REPLACEMENT: &[u8] = "\u{FFFD}".as_bytes();
#[derive(Debug, Default)]
pub(crate) struct Utf8Sanitizer {
carry: Vec<u8>,
}
impl Utf8Sanitizer {
pub(crate) fn feed(&mut self, bytes: &[u8]) -> Vec<u8> {
let mut input: Vec<u8>;
let mut rest: &[u8] = if self.carry.is_empty() {
bytes
} else {
input = std::mem::take(&mut self.carry);
input.extend_from_slice(bytes);
&input
};
let mut out = Vec::with_capacity(rest.len());
loop {
match std::str::from_utf8(rest) {
Ok(_) => {
out.extend_from_slice(rest);
break;
}
Err(e) => {
let valid = e.valid_up_to();
out.extend_from_slice(&rest[..valid]);
match e.error_len() {
Some(bad) => {
out.extend_from_slice(REPLACEMENT);
rest = &rest[valid + bad..];
}
None => {
self.carry.extend_from_slice(&rest[valid..]);
break;
}
}
}
}
}
out
}
pub(crate) fn pending(&self) -> bool {
!self.carry.is_empty()
}
pub(crate) fn finish(&mut self) -> Vec<u8> {
if self.carry.is_empty() {
return Vec::new();
}
self.carry.clear();
REPLACEMENT.to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("sanitizer output is valid UTF-8")
}
#[test]
fn valid_input_passes_through_untouched() {
let mut san = Utf8Sanitizer::default();
assert_eq!(
s(&san.feed("ascii and 汉字 and 🦀".as_bytes())),
"ascii and 汉字 and 🦀"
);
assert!(!san.pending());
}
#[test]
fn an_invalid_byte_becomes_one_replacement_and_the_columns_hold() {
let mut san = Utf8Sanitizer::default();
assert_eq!(s(&san.feed(b"caf\xe9 done")), "caf\u{FFFD} done");
assert!(!san.pending());
}
#[test]
fn a_character_split_across_two_reads_is_reassembled() {
let mut san = Utf8Sanitizer::default();
let bytes = "ab汉cd".as_bytes(); let first = san.feed(&bytes[..3]); assert_eq!(s(&first), "ab");
assert!(san.pending(), "the lead byte is held, not replaced");
let second = san.feed(&bytes[3..]);
assert_eq!(s(&second), "汉cd");
assert!(!san.pending());
}
#[test]
fn a_lead_byte_the_next_read_does_not_continue_is_replaced_then() {
let mut san = Utf8Sanitizer::default();
assert_eq!(s(&san.feed(b"ab\xe9")), "ab");
assert!(san.pending());
assert_eq!(s(&san.feed(b"cd")), "\u{FFFD}cd");
assert!(!san.pending());
}
#[test]
fn a_partial_character_at_eof_is_one_replacement() {
let mut san = Utf8Sanitizer::default();
assert_eq!(s(&san.feed(b"x\xf0\x9f")), "x");
assert!(san.pending());
assert_eq!(s(&san.finish()), "\u{FFFD}");
assert!(!san.pending());
assert!(san.finish().is_empty());
}
#[test]
fn every_invalid_sequence_is_replaced_separately() {
let mut san = Utf8Sanitizer::default();
assert_eq!(
s(&san.feed(b"a\x80\x80b\xe6\xb1c")),
"a\u{FFFD}\u{FFFD}b\u{FFFD}c"
);
}
}