use std::collections::TryReserveError;
use smol_bytes::Utf8Bytes;
pub fn lossy_len(bytes: &[u8]) -> usize {
const REPLACEMENT: usize = char::REPLACEMENT_CHARACTER.len_utf8();
let mut rest = bytes;
let mut total = 0usize;
loop {
match std::str::from_utf8(rest) {
Ok(valid) => return total + valid.len(),
Err(fault) => {
total += fault.valid_up_to() + REPLACEMENT;
match fault.error_len() {
Some(skip) => rest = &rest[fault.valid_up_to() + skip..],
None => return total,
}
}
}
}
}
pub fn lossy_text(bytes: &[u8], decoded: usize) -> Result<Utf8Bytes, TryReserveError> {
let mut buffer = std::string::String::new();
buffer.try_reserve_exact(decoded)?;
let mut rest = bytes;
loop {
match std::str::from_utf8(rest) {
Ok(valid) => {
buffer.push_str(valid);
break;
}
Err(fault) => {
let (valid, after) = rest.split_at(fault.valid_up_to());
buffer.push_str(
std::str::from_utf8(valid).expect("valid_up_to bounds a valid prefix by definition"),
);
buffer.push(char::REPLACEMENT_CHARACTER);
match fault.error_len() {
Some(skip) => rest = &after[skip..],
None => break,
}
}
}
}
debug_assert_eq!(
buffer.len(),
decoded,
"lossy_len must price exactly what lossy_text builds",
);
Ok(Utf8Bytes::from(buffer))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lossy_len_prices_exactly_what_lossy_text_builds() {
let cases: [&[u8]; 9] = [
b"",
b"Opening",
"héllo wörld".as_bytes(),
b"\xff",
b"\xff\xfe\xfd",
b"before\xffafter",
b"\xe2\x82", b"ok\xe2\x82", b"\xf0\x9f\x92\xa9ok", ];
for raw in cases {
let built = std::string::String::from_utf8_lossy(raw);
assert_eq!(
lossy_len(raw),
built.len(),
"{raw:?} must be priced at what from_utf8_lossy produces",
);
let text = lossy_text(raw, lossy_len(raw)).expect("a small reservation");
assert_eq!(
text.as_str(),
built.as_ref(),
"{raw:?} must decode identically"
);
}
}
}