#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Order {
Little,
Big,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Bom {
Required,
Prohibited(Order),
Written(Order),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct Encoding {
width: usize,
bom: Bom,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum Failure {
BomRequired,
BomProhibited,
Invalid,
}
const PLATFORM: Order = if cfg!(target_os = "macos") || cfg!(target_endian = "big") {
Order::Big
} else {
Order::Little
};
impl Encoding {
pub(super) fn parse(name: &[u8]) -> Result<Option<Self>, ()> {
let name = name.to_ascii_uppercase();
let (width, rest) = match name.as_slice() {
b"UTF-8" | b"UTF8" => return Ok(None),
_ if name.starts_with(b"UTF-16") => (2, &name[6..]),
_ if name.starts_with(b"UTF16") => (2, &name[5..]),
_ if name.starts_with(b"UTF-32") => (4, &name[6..]),
_ if name.starts_with(b"UTF32") => (4, &name[5..]),
_ => return Err(()),
};
let bom = match rest {
b"" => Bom::Required,
b"LE" => Bom::Prohibited(Order::Little),
b"BE" => Bom::Prohibited(Order::Big),
b"LE-BOM" => Bom::Written(Order::Little),
b"BE-BOM" => Bom::Written(Order::Big),
_ => return Err(()),
};
Ok(Some(Self { width, bom }))
}
fn mark(&self, order: Order) -> Vec<u8> {
unit(0xfeff, self.width, order)
}
fn starting_order(&self, bytes: &[u8]) -> Option<Order> {
[Order::Little, Order::Big]
.into_iter()
.find(|order| bytes.starts_with(&self.mark(*order)))
}
pub(super) fn decode(&self, bytes: &[u8]) -> Result<Vec<u8>, Failure> {
if bytes.is_empty() {
return Ok(Vec::new());
}
let found = self.starting_order(bytes);
let (order, body) = match (self.bom, found) {
(Bom::Required, Some(order)) => (order, &bytes[self.width..]),
(Bom::Required, None) => return Err(Failure::BomRequired),
(Bom::Prohibited(_), Some(_)) => return Err(Failure::BomProhibited),
(Bom::Prohibited(order), None) => (order, bytes),
(Bom::Written(_), Some(found)) => (found, &bytes[self.width..]),
(Bom::Written(_), None) => (PLATFORM, bytes),
};
if body.len() % self.width != 0 {
return Err(Failure::Invalid);
}
let mut units = body
.chunks_exact(self.width)
.map(|chunk| read(chunk, order));
let mut text = String::with_capacity(body.len() / self.width);
while let Some(unit) = units.next() {
let scalar = match unit {
0xd800..=0xdbff if self.width == 2 => {
let low = units.next().ok_or(Failure::Invalid)?;
if !(0xdc00..=0xdfff).contains(&low) {
return Err(Failure::Invalid);
}
0x10000 + ((unit - 0xd800) << 10) + (low - 0xdc00)
}
_ => unit,
};
text.push(char::from_u32(scalar).ok_or(Failure::Invalid)?);
}
Ok(text.into_bytes())
}
pub(super) fn encode(&self, bytes: &[u8]) -> Result<Vec<u8>, Failure> {
let text = std::str::from_utf8(bytes).map_err(|_| Failure::Invalid)?;
if text.is_empty() {
return Ok(Vec::new());
}
let (order, mut out) = match self.bom {
Bom::Required => (PLATFORM, self.mark(PLATFORM)),
Bom::Written(order) => (order, self.mark(order)),
Bom::Prohibited(order) => (order, Vec::new()),
};
out.reserve(text.len() * self.width);
if self.width == 2 {
for unit in text.encode_utf16() {
out.extend(unit_bytes(u32::from(unit), 2, order));
}
} else {
for scalar in text.chars() {
out.extend(unit_bytes(u32::from(scalar), 4, order));
}
}
Ok(out)
}
}
fn read(chunk: &[u8], order: Order) -> u32 {
let fold = |value: u32, byte: &u8| (value << 8) | u32::from(*byte);
match order {
Order::Big => chunk.iter().fold(0, fold),
Order::Little => chunk.iter().rev().fold(0, fold),
}
}
fn unit_bytes(value: u32, width: usize, order: Order) -> impl Iterator<Item = u8> {
let bytes = value.to_be_bytes();
let bytes = bytes.into_iter().skip(4 - width);
let mut bytes: Vec<u8> = bytes.collect();
if order == Order::Little {
bytes.reverse();
}
bytes.into_iter()
}
fn unit(value: u32, width: usize, order: Order) -> Vec<u8> {
unit_bytes(value, width, order).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn encoding(name: &str) -> Encoding {
Encoding::parse(name.as_bytes()).unwrap().unwrap()
}
#[test]
fn names_follow_git() {
assert_eq!(Encoding::parse(b"utf-8"), Ok(None));
assert!(Encoding::parse(b"latin1").is_err());
assert!(Encoding::parse(b"UTF-16XE").is_err());
assert_eq!(encoding("utf-16le"), encoding("UTF16LE"));
}
#[test]
fn decoding_matches_git_add() {
let stored = "hé\n".as_bytes();
let cases: &[(&str, &[u8])] = &[
("UTF-16", b"\xfe\xff\x00h\x00\xe9\x00\n"),
("UTF-16", b"\xff\xfeh\x00\xe9\x00\n\x00"),
("UTF-16LE", b"h\x00\xe9\x00\n\x00"),
("UTF-16BE", b"\x00h\x00\xe9\x00\n"),
("UTF-16LE-BOM", b"\xff\xfeh\x00\xe9\x00\n\x00"),
("UTF-16LE-BOM", b"\xfe\xff\x00h\x00\xe9\x00\n"),
("UTF-32LE", b"h\x00\x00\x00\xe9\x00\x00\x00\n\x00\x00\x00"),
];
for (name, bytes) in cases {
assert_eq!(encoding(name).decode(bytes).unwrap(), stored, "{name}");
}
assert_eq!(
encoding("UTF-16").decode(b"h\x00"),
Err(Failure::BomRequired)
);
assert_eq!(
encoding("UTF-16LE").decode(b"\xff\xfeh\x00"),
Err(Failure::BomProhibited)
);
assert_eq!(
encoding("UTF-16LE").decode(b"h\x00\x00"),
Err(Failure::Invalid)
);
assert_eq!(
encoding("UTF-16LE").decode(b"\x00\xd8a\x00"),
Err(Failure::Invalid)
);
assert_eq!(encoding("UTF-16").decode(b""), Ok(Vec::new()));
}
#[test]
fn unmarked_bom_variant_uses_the_platform_order() {
let decoded = encoding("UTF-16LE-BOM").decode(b"\x00h").unwrap();
let expected = if PLATFORM == Order::Big {
"h"
} else {
"\u{6800}"
};
assert_eq!(decoded, expected.as_bytes());
}
#[test]
fn encoding_round_trips_and_marks() {
let text = "h\u{1F600}é\n".as_bytes();
for name in ["UTF-16", "UTF-16LE", "UTF-16BE-BOM", "UTF-32", "UTF-32BE"] {
let encoded = encoding(name).encode(text).unwrap();
assert_eq!(encoding(name).decode(&encoded).unwrap(), text, "{name}");
}
assert_eq!(
encoding("UTF-16LE-BOM").encode(b"h").unwrap(),
b"\xff\xfeh\x00"
);
assert_eq!(encoding("UTF-16").encode(b"").unwrap(), b"");
assert_eq!(encoding("UTF-16LE").encode(b"\xff"), Err(Failure::Invalid));
}
}