use std::borrow::Cow;
#[must_use]
pub fn decode(raw: Vec<u8>) -> Vec<u8> {
match recognized(&raw) {
Recognized::Marked(encoding, mark) => decode_units(&raw[mark..], encoding),
Recognized::Unmarked(_, text) => text.into_bytes(),
Recognized::Bytes => raw,
}
}
#[must_use]
pub fn text_of(raw: &[u8]) -> Cow<'_, [u8]> {
match recognized(raw) {
Recognized::Marked(Encoding::Utf8, mark) => Cow::Borrowed(&raw[mark..]),
Recognized::Marked(encoding, mark) => Cow::Owned(decode_units(&raw[mark..], encoding)),
Recognized::Unmarked(_, text) => Cow::Owned(text.into_bytes()),
Recognized::Bytes => Cow::Borrowed(raw),
}
}
#[must_use]
pub fn read_as(raw: &[u8]) -> (Encoding, usize) {
match recognized(raw) {
Recognized::Marked(encoding, mark) => (encoding, mark),
Recognized::Unmarked(encoding, _) => (encoding, 0),
Recognized::Bytes => (Encoding::Utf8, 0),
}
}
enum Recognized {
Marked(Encoding, usize),
Unmarked(Encoding, String),
Bytes,
}
fn recognized(raw: &[u8]) -> Recognized {
let (encoding, mark) = Encoding::declared(raw);
if mark > 0 {
return Recognized::Marked(encoding, mark);
}
if !raw.contains(&0) && std::str::from_utf8(raw).is_ok() {
return Recognized::Bytes;
}
if raw.len() < 8 {
return Recognized::Bytes;
}
let le = strict_utf16(raw, |c| u16::from_le_bytes(*c));
let be = strict_utf16(raw, |c| u16::from_be_bytes(*c));
match (le, be) {
(Some(s), None) => Recognized::Unmarked(Encoding::Utf16Le, s),
(None, Some(s)) => Recognized::Unmarked(Encoding::Utf16Be, s),
(Some(l), Some(b)) => {
let (even_nulls, odd_nulls) = null_parity(raw);
if odd_nulls > even_nulls.saturating_mul(4) {
Recognized::Unmarked(Encoding::Utf16Le, l)
} else if even_nulls > odd_nulls.saturating_mul(4) {
Recognized::Unmarked(Encoding::Utf16Be, b)
} else {
Recognized::Unmarked(Encoding::Utf16Le, l)
}
}
(None, None) => Recognized::Bytes,
}
}
#[must_use]
pub(crate) fn encode(text: &[u8], encoding: Encoding) -> Cow<'_, [u8]> {
let chars = || String::from_utf8_lossy(text);
match encoding {
Encoding::Utf8 => Cow::Borrowed(text),
Encoding::Utf16Le => Cow::Owned(chars().encode_utf16().flat_map(u16::to_le_bytes).collect()),
Encoding::Utf16Be => Cow::Owned(chars().encode_utf16().flat_map(u16::to_be_bytes).collect()),
Encoding::Utf32Le => Cow::Owned(chars().chars().flat_map(|c| u32::from(c).to_le_bytes()).collect()),
Encoding::Utf32Be => Cow::Owned(chars().chars().flat_map(|c| u32::from(c).to_be_bytes()).collect()),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Run {
Same(usize),
Unit { bytes: usize, text: usize },
}
impl Run {
fn bytes(self) -> usize {
match self {
Run::Same(n) | Run::Unit { bytes: n, .. } => n,
}
}
fn text(self) -> usize {
match self {
Run::Same(n) | Run::Unit { text: n, .. } => n,
}
}
}
struct Runs<'a> {
units: &'a [u8],
encoding: Encoding,
lossy: bool,
at: usize,
}
impl Iterator for Runs<'_> {
type Item = Run;
fn next(&mut self) -> Option<Run> {
let rest = &self.units[self.at..];
let run = match self.encoding {
_ if rest.is_empty() => return None,
Encoding::Utf8 if !self.lossy => Run::Same(rest.len()),
Encoding::Utf8 => match rest.utf8_chunks().next() {
Some(chunk) if chunk.valid().is_empty() => {
Run::Unit { bytes: chunk.invalid().len(), text: char::REPLACEMENT_CHARACTER.len_utf8() }
}
Some(chunk) => Run::Same(chunk.valid().len()),
None => return None,
},
Encoding::Utf16Le | Encoding::Utf16Be => {
let order = if self.encoding == Encoding::Utf16Le { u16::from_le_bytes } else { u16::from_be_bytes };
let unit = |i: usize| rest.get(i..i + 2).map(|c| order([c[0], c[1]]));
let high = |u: u16| (0xD800..0xDC00).contains(&u);
let low = |u: u16| (0xDC00..0xE000).contains(&u);
match unit(0) {
None => Run::Unit { bytes: rest.len(), text: 0 },
Some(u) if high(u) && unit(2).is_some_and(low) => Run::Unit { bytes: 4, text: 4 },
Some(u) => Run::Unit {
bytes: 2,
text: char::from_u32(u32::from(u)).map_or(char::REPLACEMENT_CHARACTER.len_utf8(), char::len_utf8),
},
}
}
Encoding::Utf32Le | Encoding::Utf32Be => {
let order = if self.encoding == Encoding::Utf32Le { u32::from_le_bytes } else { u32::from_be_bytes };
match rest.first_chunk::<4>() {
None => Run::Unit { bytes: rest.len(), text: 0 },
Some(c) => Run::Unit { bytes: 4, text: char::from_u32(order(*c)).map_or(0, char::len_utf8) },
}
}
};
self.at += run.bytes();
Some(run)
}
}
pub(crate) struct Places<'a> {
runs: Runs<'a>,
run: Option<Run>,
byte: usize,
text: usize,
}
impl<'a> Places<'a> {
#[must_use]
pub(crate) fn new(units: &'a [u8], encoding: Encoding, lossy: bool) -> Self {
let mut runs = Runs { units, encoding, lossy, at: 0 };
let run = runs.next();
Places { runs, run, byte: 0, text: 0 }
}
pub(crate) fn earliest(&mut self, offset: usize) -> usize {
self.find(offset, false)
}
pub(crate) fn latest(&mut self, offset: usize) -> usize {
self.find(offset, true)
}
fn find(&mut self, offset: usize, past_empty: bool) -> usize {
while let Some(run) = self.run {
let inside = offset > self.text && offset < self.text + run.text();
if inside {
return match run {
Run::Same(_) => self.byte + (offset - self.text),
Run::Unit { .. } => self.byte,
};
}
let passed = offset > self.text || (past_empty && offset == self.text && run.text() == 0);
if !passed {
return self.byte;
}
self.byte += run.bytes();
self.text += run.text();
self.run = self.runs.next();
}
self.byte
}
}
#[must_use]
pub fn decode_units(raw: &[u8], encoding: Encoding) -> Vec<u8> {
match encoding {
Encoding::Utf8 => raw.to_vec(),
Encoding::Utf16Le => utf16_text(raw, u16::from_le_bytes),
Encoding::Utf16Be => utf16_text(raw, u16::from_be_bytes),
Encoding::Utf32Le => utf32_text(raw, u32::from_le_bytes),
Encoding::Utf32Be => utf32_text(raw, u32::from_be_bytes),
}
}
fn utf16_text(raw: &[u8], unit: impl Fn([u8; 2]) -> u16) -> Vec<u8> {
let units: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| unit(*c)).collect();
String::from_utf16_lossy(&units).into_bytes()
}
fn utf32_text(raw: &[u8], unit: impl Fn([u8; 4]) -> u32) -> Vec<u8> {
raw.as_chunks::<4>().0.iter().filter_map(|c| char::from_u32(unit(*c))).collect::<String>().into_bytes()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Encoding {
#[default]
Utf8,
Utf16Le,
Utf16Be,
Utf32Le,
Utf32Be,
}
impl Encoding {
#[must_use]
pub fn declared(opening: &[u8]) -> (Encoding, usize) {
if opening.starts_with(&[0xFF, 0xFE, 0x00, 0x00]) {
(Encoding::Utf32Le, 4)
} else if opening.starts_with(&[0x00, 0x00, 0xFE, 0xFF]) {
(Encoding::Utf32Be, 4)
} else if opening.starts_with(&[0xFF, 0xFE]) {
(Encoding::Utf16Le, 2)
} else if opening.starts_with(&[0xFE, 0xFF]) {
(Encoding::Utf16Be, 2)
} else if opening.starts_with(&[0xEF, 0xBB, 0xBF]) {
(Encoding::Utf8, 3)
} else {
(Encoding::Utf8, 0)
}
}
#[must_use]
pub fn unit_width(self) -> usize {
match self {
Encoding::Utf8 => 1,
Encoding::Utf16Le | Encoding::Utf16Be => 2,
Encoding::Utf32Le | Encoding::Utf32Be => 4,
}
}
#[must_use]
pub fn newline(self) -> &'static [u8] {
match self {
Encoding::Utf8 => b"\n",
Encoding::Utf16Le => &[0x0A, 0x00],
Encoding::Utf16Be => &[0x00, 0x0A],
Encoding::Utf32Le => &[0x0A, 0x00, 0x00, 0x00],
Encoding::Utf32Be => &[0x00, 0x00, 0x00, 0x0A],
}
}
}
#[must_use]
pub fn utf16_units(text: &[u8]) -> usize {
text.iter().map(|&b| usize::from((b & 0xC0) != 0x80) + usize::from(b >= 0xF0)).sum()
}
fn opens_a_longer_mark(bytes: &[u8]) -> bool {
const MARKS: [&[u8]; 5] =
[&[0xFF, 0xFE, 0x00, 0x00], &[0x00, 0x00, 0xFE, 0xFF], &[0xFF, 0xFE], &[0xFE, 0xFF], &[0xEF, 0xBB, 0xBF]];
MARKS.iter().any(|mark| mark.len() > bytes.len() && mark.starts_with(bytes))
}
#[derive(Clone, Debug, Default)]
pub struct Incremental {
encoding: Option<Encoding>,
held: Vec<u8>,
}
impl Incremental {
#[must_use]
pub fn from_start() -> Self {
Incremental { encoding: None, held: Vec::new() }
}
#[must_use]
pub fn after(encoding: Encoding) -> Self {
Incremental { encoding: Some(encoding), held: Vec::new() }
}
pub fn decode(&mut self, piece: &[u8]) -> Vec<u8> {
self.held.extend_from_slice(piece);
let encoding = match self.encoding {
Some(e) => e,
None => {
if opens_a_longer_mark(&self.held) {
return Vec::new();
}
let (e, mark) = Encoding::declared(&self.held);
self.held.drain(..mark);
self.encoding = Some(e);
e
}
};
match encoding {
Encoding::Utf8 => std::mem::take(&mut self.held),
Encoding::Utf16Le => self.take_utf16(u16::from_le_bytes),
Encoding::Utf16Be => self.take_utf16(u16::from_be_bytes),
Encoding::Utf32Le | Encoding::Utf32Be => {
let taken = self.held.len() / 4 * 4;
let text = decode_units(&self.held[..taken], encoding);
self.held.drain(..taken);
text
}
}
}
fn take_utf16(&mut self, unit: impl Fn([u8; 2]) -> u16) -> Vec<u8> {
let mut units: Vec<u16> = self.held.as_chunks::<2>().0.iter().map(|c| unit(*c)).collect();
if units.last().is_some_and(|u| (0xD800..0xDC00).contains(u)) {
units.pop();
}
self.held.drain(..units.len() * 2);
String::from_utf16_lossy(&units).into_bytes()
}
pub fn finish(&mut self) -> Vec<u8> {
let held = std::mem::take(&mut self.held);
match self.encoding {
Some(encoding) => decode_units(&held, encoding),
None => decode(held),
}
}
}
fn strict_utf16(raw: &[u8], unit: fn(&[u8; 2]) -> u16) -> Option<String> {
if raw.is_empty() || !raw.len().is_multiple_of(2) {
return None;
}
let s: String =
char::decode_utf16(raw.as_chunks::<2>().0.iter().map(unit)).collect::<Result<_, _>>().ok()?;
if s.chars().any(|c| c.is_control() && !matches!(c, '\t' | '\n' | '\r')) {
return None;
}
if !s.chars().any(char::is_alphabetic) {
return None;
}
let mut counts: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
let mut bucketed = 0usize;
for c in s.chars() {
let cp = c as u32;
let common = c.is_ascii()
|| c.is_whitespace()
|| matches!(cp, 0x2000..=0x206F | 0x3000..=0x303F | 0xFF00..=0xFFEF | 0x20A0..=0x20CF);
if common {
continue;
}
bucketed += 1;
*counts.entry(script_bucket(c)).or_default() += 1;
}
if bucketed > 0 {
let mut by_count: Vec<usize> = counts.into_values().collect();
by_count.sort_unstable_by(|a, b| b.cmp(a));
let allowed = if bucketed < 16 { 1 } else { 2 };
let top: usize = by_count.iter().take(allowed).sum();
if top * 100 < bucketed * 95 {
return None;
}
}
Some(s)
}
fn script_bucket(c: char) -> u32 {
let cp = c as u32;
match cp {
0x0041..=0x024F | 0x1E00..=0x1EFF => 1, 0x0370..=0x03FF | 0x1F00..=0x1FFF => 2, 0x0400..=0x052F => 3, 0x0530..=0x058F => 4, 0x0590..=0x05FF => 5, 0x0600..=0x077F | 0x08A0..=0x08FF => 6, 0x0900..=0x0DFF => 7 + ((cp - 0x0900) >> 7), 0x0E00..=0x0E7F => 30, 0x0E80..=0x0EFF => 31, 0x0F00..=0x0FFF => 32, 0x1000..=0x109F => 33, 0x10A0..=0x10FF => 34, 0x1200..=0x139F => 35, 0x3040..=0x30FF | 0x31F0..=0x31FF => 36, 0x1100..=0x11FF | 0x3130..=0x318F | 0xAC00..=0xD7FF => 37, 0x2E80..=0x2FDF | 0x3400..=0x9FFF | 0xF900..=0xFAFF => 38, _ => 1000 + (cp >> 11), }
}
fn null_parity(raw: &[u8]) -> (usize, usize) {
let mut even = 0;
let mut odd = 0;
for (i, &b) in raw.iter().enumerate() {
if b == 0 {
if i.is_multiple_of(2) { even += 1 } else { odd += 1 }
}
}
(even, odd)
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
fn utf16be(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_be_bytes).collect()
}
#[test]
fn boms_select_the_encoding() {
let text = "hi \u{4E2D}\u{6587}"; let mut le = vec![0xFF, 0xFE];
le.extend(utf16le(text));
assert_eq!(decode(le), text.as_bytes());
let mut be = vec![0xFE, 0xFF];
be.extend(utf16be(text));
assert_eq!(decode(be), text.as_bytes());
let mut u8bom = vec![0xEF, 0xBB, 0xBF];
u8bom.extend(text.as_bytes());
assert_eq!(decode(u8bom), text.as_bytes());
let mut u32le = vec![0xFF, 0xFE, 0x00, 0x00];
u32le.extend(text.chars().flat_map(|c| (c as u32).to_le_bytes()));
assert_eq!(decode(u32le), text.as_bytes());
let mut u32be = vec![0x00, 0x00, 0xFE, 0xFF];
u32be.extend(text.chars().flat_map(|c| (c as u32).to_be_bytes()));
assert_eq!(decode(u32be), text.as_bytes());
}
#[test]
fn bomless_utf16_ascii_both_endians() {
let text = "call f(42) ok\n";
assert_eq!(decode(utf16le(text)), text.as_bytes());
assert_eq!(decode(utf16be(text)), text.as_bytes());
}
#[test]
fn bomless_utf16_cjk() {
let text = "\u{4E2D}\u{6587}\u{5206}\u{8BCD} ok\n";
assert_eq!(decode(utf16le(text)), text.as_bytes());
assert_eq!(decode(utf16be(text)), text.as_bytes());
let pure = "\u{4E2D}\u{6587}\u{5206}\u{8BCD}";
assert_eq!(decode(utf16le(pure)), pure.as_bytes());
}
#[test]
fn utf8_and_ascii_pass_untouched() {
for text in ["plain ascii", "caf\u{E9} \u{4E2D}\u{6587}", ""] {
assert_eq!(decode(text.as_bytes().to_vec()), text.as_bytes());
}
}
#[test]
fn an_input_decoded_in_pieces_reads_as_the_whole_decoded_at_once() {
let text = "a \u{4E2D}\u{6587} \u{1F600} line\nnext\n";
let mut inputs: Vec<Vec<u8>> = Vec::new();
let mut le = vec![0xFF, 0xFE];
le.extend(utf16le(text));
inputs.push(le);
let mut be = vec![0xFE, 0xFF];
be.extend(utf16be(text));
inputs.push(be);
let mut u32le = vec![0xFF, 0xFE, 0x00, 0x00];
u32le.extend(text.chars().flat_map(|c| (c as u32).to_le_bytes()));
inputs.push(u32le);
let mut u32be = vec![0x00, 0x00, 0xFE, 0xFF];
u32be.extend(text.chars().flat_map(|c| (c as u32).to_be_bytes()));
inputs.push(u32be);
let mut u8bom = vec![0xEF, 0xBB, 0xBF];
u8bom.extend(text.as_bytes());
inputs.push(u8bom);
inputs.push(text.as_bytes().to_vec());
for input in &inputs {
for piece in 1..=5 {
let mut decoder = Incremental::from_start();
let mut out = Vec::new();
for chunk in input.chunks(piece) {
out.extend(decoder.decode(chunk));
}
assert_eq!(out, decode(input.clone()), "{:02X?} in pieces of {piece}", &input[..4]);
}
}
let mut decoder = Incremental::after(Encoding::Utf16Le);
let body = utf16le(text);
let mut out = decoder.decode(&body[..3]);
out.extend(decoder.decode(&body[3..]));
assert_eq!(out, text.as_bytes());
}
#[test]
fn utf16_units_count_what_a_utf16_string_holds() {
for text in ["", "plain", "caf\u{E9}", "\u{4E2D}\u{6587}", "a\u{1F600}b\n", "\u{10FFFF}"] {
assert_eq!(utf16_units(text.as_bytes()), text.encode_utf16().count(), "{text:?}");
}
}
#[test]
fn binary_is_never_misread() {
assert_eq!(decode(vec![0xFF, 0x00, 0xFF]), vec![0xFF, 0x00, 0xFF]);
let ctl = vec![0x07, 0x00, 0x08, 0x00, 0x61, 0x00, 0x07, 0x00];
assert_eq!(decode(ctl.clone()), ctl);
let surr = vec![0x00, 0xD8, 0x61, 0x00];
assert_eq!(decode(surr.clone()), surr);
let png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
assert_eq!(decode(png.clone()), png);
let tiny = vec![0x2D, 0x4E];
assert_eq!(decode(tiny.clone()), tiny);
let mut x = 0x1234_5678_u32;
let blob: Vec<u8> = (0..64)
.map(|_| {
x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
(x >> 24) as u8
})
.collect();
assert_eq!(decode(blob.clone()), blob);
}
}