pub(super) const MAX_CONSOLE_WRITE_UNITS: usize = 4096;
const MAX_UTF8_CARRY: usize = 3;
const REPLACEMENT: u16 = 0xFFFD;
#[derive(Debug, Default)]
pub(super) struct Utf8ToUtf16Decoder {
carry: Vec<u8>,
}
#[derive(Debug, Default)]
pub(super) struct Utf16ToUtf8Decoder {
carry: Option<u16>,
}
impl Utf8ToUtf16Decoder {
pub(super) fn decode(&mut self, bytes: &[u8]) -> Vec<u16> {
let mut units = Vec::with_capacity(bytes.len());
let joined: Vec<u8>;
let mut rest: &[u8] = if self.carry.is_empty() {
bytes
} else {
joined = self
.carry
.iter()
.copied()
.chain(bytes.iter().copied())
.collect();
self.carry.clear();
&joined
};
loop {
match std::str::from_utf8(rest) {
Ok(text) => {
units.extend(text.encode_utf16());
return units;
}
Err(error) => {
let valid = error.valid_up_to();
if valid > 0 {
let text = std::str::from_utf8(&rest[..valid])
.expect("valid_up_to marks a valid UTF-8 prefix");
units.extend(text.encode_utf16());
}
match error.error_len() {
None => {
let tail = &rest[valid..];
debug_assert!(
tail.len() <= MAX_UTF8_CARRY,
"incomplete UTF-8 tail cannot exceed {MAX_UTF8_CARRY} bytes"
);
self.carry.extend_from_slice(tail);
return units;
}
Some(invalid_len) => {
units.push(REPLACEMENT);
rest = &rest[valid + invalid_len..];
}
}
}
}
}
}
pub(super) fn finish(&mut self) -> Vec<u16> {
if self.carry.is_empty() {
return Vec::new();
}
let units = String::from_utf8_lossy(&self.carry)
.encode_utf16()
.collect();
self.carry.clear();
units
}
}
impl Utf16ToUtf8Decoder {
pub(super) fn decode(&mut self, units: &[u16]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(units.len());
let mut buf = [0u8; 4];
let mut pending = self.carry.take();
for &unit in units {
if let Some(high) = pending.take() {
if is_low_surrogate(unit) {
let code =
0x1_0000 + (u32::from(high - 0xD800) << 10) + u32::from(unit - 0xDC00);
let ch = char::from_u32(code).unwrap_or(char::REPLACEMENT_CHARACTER);
bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
continue;
}
bytes.extend_from_slice(
char::REPLACEMENT_CHARACTER.encode_utf8(&mut buf).as_bytes(),
);
}
if is_high_surrogate(unit) {
pending = Some(unit);
} else if is_low_surrogate(unit) {
bytes.extend_from_slice(
char::REPLACEMENT_CHARACTER.encode_utf8(&mut buf).as_bytes(),
);
} else {
let ch = char::from_u32(u32::from(unit)).unwrap_or(char::REPLACEMENT_CHARACTER);
bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
}
}
self.carry = pending;
bytes
}
}
pub(super) fn surrogate_safe_split(units: &[u16], max: usize) -> usize {
if units.len() <= max {
return units.len();
}
if max == 0 {
return 0;
}
if split_separates_surrogate_pair(units, max) {
max - 1
} else {
max
}
}
pub(super) fn split_separates_surrogate_pair(units: &[u16], index: usize) -> bool {
index > 0
&& index < units.len()
&& is_high_surrogate(units[index - 1])
&& is_low_surrogate(units[index])
}
fn is_high_surrogate(unit: u16) -> bool {
(0xD800..0xDC00).contains(&unit)
}
fn is_low_surrogate(unit: u16) -> bool {
(0xDC00..0xE000).contains(&unit)
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16(text: &str) -> Vec<u16> {
text.encode_utf16().collect()
}
#[test]
fn test_utf8_decoder_passes_through_ascii() {
let mut decoder = Utf8ToUtf16Decoder::default();
assert_eq!(decoder.decode(b"hello world"), utf16("hello world"));
}
#[test]
fn test_utf8_decoder_preserves_ansi_escapes() {
let mut decoder = Utf8ToUtf16Decoder::default();
let ansi = "\x1b[31merror\x1b[0m\x1b[2J\x1b[H";
assert_eq!(decoder.decode(ansi.as_bytes()), utf16(ansi));
}
#[test]
fn test_utf8_decoder_decodes_issue_repro() {
let bytes = [
0x75, 0x6e, 0x69, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x20, 0xe2, 0x80, 0x94, 0x20, 0xe2,
0x9c, 0x93, 0x20, 0xe2, 0xa0, 0x8b, 0x0a,
];
let mut decoder = Utf8ToUtf16Decoder::default();
assert_eq!(decoder.decode(&bytes), utf16("unicode: — ✓ ⠋\n"));
}
#[test]
fn test_utf8_decoder_joins_sequence_split_across_chunks() {
let mut decoder = Utf8ToUtf16Decoder::default();
assert!(decoder.decode(&[0xE2]).is_empty());
assert_eq!(decoder.decode(&[0x80, 0x94]), utf16("—"));
}
#[test]
fn test_utf8_decoder_joins_sequences_split_at_every_offset() {
let text = "unicode: — ✓ ⠋ 😀\n";
for split in 0..=text.len() {
let mut decoder = Utf8ToUtf16Decoder::default();
let mut units = decoder.decode(&text.as_bytes()[..split]);
units.extend(decoder.decode(&text.as_bytes()[split..]));
units.extend(decoder.finish());
assert_eq!(
String::from_utf16(&units).unwrap(),
text,
"split at byte {split}"
);
}
}
#[test]
fn test_utf8_decoder_joins_sequence_split_one_byte_at_a_time() {
let mut decoder = Utf8ToUtf16Decoder::default();
let mut units = Vec::new();
for byte in "⠋".as_bytes() {
units.extend(decoder.decode(&[*byte]));
}
assert_eq!(units, utf16("⠋"));
}
#[test]
fn test_utf8_decoder_replaces_invalid_bytes() {
let mut decoder = Utf8ToUtf16Decoder::default();
assert_eq!(decoder.decode(&[0xFF]), vec![REPLACEMENT]);
}
#[test]
fn test_utf8_decoder_keeps_going_after_invalid_bytes() {
let mut decoder = Utf8ToUtf16Decoder::default();
let mut expected = utf16("ok");
expected.push(REPLACEMENT);
expected.extend(utf16("more"));
assert_eq!(decoder.decode(b"ok\xFFmore"), expected);
}
#[test]
fn test_utf8_decoder_replaces_truncated_sequence_then_resumes() {
let mut decoder = Utf8ToUtf16Decoder::default();
let mut expected = vec![REPLACEMENT];
expected.extend(utf16("a"));
assert_eq!(decoder.decode(&[0xE2, 0x61]), expected);
}
#[test]
fn test_utf8_decoder_survives_binary_data() {
let mut decoder = Utf8ToUtf16Decoder::default();
let garbage: Vec<u8> = (0..=255u8).collect();
assert!(!decoder.decode(&garbage).is_empty());
assert_eq!(decoder.decode(b"after"), utf16("after"));
}
#[test]
fn test_utf8_decoder_finish_flushes_incomplete_tail() {
let mut decoder = Utf8ToUtf16Decoder::default();
assert!(decoder.decode(&[0xE2, 0x80]).is_empty());
assert_eq!(decoder.finish(), vec![REPLACEMENT]);
assert!(decoder.finish().is_empty());
}
#[test]
fn test_utf8_decoder_finish_is_empty_without_carry() {
let mut decoder = Utf8ToUtf16Decoder::default();
assert_eq!(decoder.decode(b"complete"), utf16("complete"));
assert!(decoder.finish().is_empty());
}
#[test]
fn test_utf16_decoder_encodes_bmp_text() {
let mut decoder = Utf16ToUtf8Decoder::default();
assert_eq!(
decoder.decode(&utf16("héllo 日本語")),
"héllo 日本語".as_bytes()
);
}
#[test]
fn test_utf16_decoder_joins_surrogate_pair_across_chunks() {
let units = utf16("🚀");
assert_eq!(units.len(), 2, "rocket must be a surrogate pair");
let mut decoder = Utf16ToUtf8Decoder::default();
assert!(decoder.decode(&units[..1]).is_empty());
assert_eq!(decoder.decode(&units[1..]), "🚀".as_bytes());
}
#[test]
fn test_utf16_decoder_replaces_unpaired_low_surrogate() {
let mut decoder = Utf16ToUtf8Decoder::default();
assert_eq!(
decoder.decode(&[0xDC00]),
char::REPLACEMENT_CHARACTER.to_string().as_bytes()
);
}
#[test]
fn test_utf16_decoder_replaces_high_surrogate_without_pair() {
let mut decoder = Utf16ToUtf8Decoder::default();
assert!(decoder.decode(&[0xD83D]).is_empty());
let mut expected = char::REPLACEMENT_CHARACTER.to_string().into_bytes();
expected.extend_from_slice(b"a");
assert_eq!(decoder.decode(&utf16("a")), expected);
}
#[test]
fn test_utf16_decoder_preserves_ascii_control_bytes() {
let mut decoder = Utf16ToUtf8Decoder::default();
assert_eq!(decoder.decode(&[0x1D]), vec![0x1D]);
assert_eq!(decoder.decode(&[0x10, 0x11]), vec![0x10, 0x11]);
}
#[test]
fn test_round_trip_through_both_decoders_one_byte_at_a_time() {
let text = "ascii — ✓ ⠋ 日本語 🚀 done";
let mut to_utf16 = Utf8ToUtf16Decoder::default();
let mut to_utf8 = Utf16ToUtf8Decoder::default();
let mut bytes = Vec::new();
for byte in text.as_bytes() {
let units = to_utf16.decode(&[*byte]);
bytes.extend(to_utf8.decode(&units));
}
assert_eq!(String::from_utf8(bytes).unwrap(), text);
}
#[test]
fn test_surrogate_safe_split_returns_len_when_under_max() {
let units = utf16("short");
assert_eq!(
surrogate_safe_split(&units, MAX_CONSOLE_WRITE_UNITS),
units.len()
);
}
#[test]
fn test_surrogate_safe_split_cuts_at_max_on_bmp_text() {
let units = utf16("aaaaaa");
assert_eq!(surrogate_safe_split(&units, 4), 4);
}
#[test]
fn test_surrogate_safe_split_backs_off_a_split_pair() {
let units = utf16("a🚀a");
assert!(is_high_surrogate(units[1]));
assert_eq!(surrogate_safe_split(&units, 2), 1);
assert_eq!(surrogate_safe_split(&units, 3), 3);
}
#[test]
fn test_surrogate_safe_split_never_splits_a_pair() {
let units = utf16("🚀🚀🚀🚀");
for max in 1..units.len() {
let split = surrogate_safe_split(&units, max);
assert!(
split == 0 || !is_high_surrogate(units[split - 1]),
"max {max} split at {split}, orphaning a high surrogate"
);
}
}
#[test]
fn test_surrogate_safe_split_handles_zero_max() {
assert_eq!(surrogate_safe_split(&utf16("abc"), 0), 0);
}
#[test]
fn test_split_separates_surrogate_pair_detects_partial_write_boundary() {
let units = utf16("a🚀a");
assert!(split_separates_surrogate_pair(&units, 2));
assert!(!split_separates_surrogate_pair(&units, 0));
assert!(!split_separates_surrogate_pair(&units, 1));
assert!(!split_separates_surrogate_pair(&units, 3));
assert!(!split_separates_surrogate_pair(&units, units.len()));
}
}