1use std::io::{Error, ErrorKind, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Encoding {
11 Utf8,
12 Utf16,
14 Utf16Le,
15 Utf16Be,
16}
17
18impl std::str::FromStr for Encoding {
19 type Err = String;
20
21 fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
22 match name.to_ascii_lowercase().as_str() {
23 "utf-8" => Ok(Self::Utf8),
24 "utf-16" => Ok(Self::Utf16),
25 "utf-16le" => Ok(Self::Utf16Le),
26 "utf-16be" => Ok(Self::Utf16Be),
27 _ => Err(format!(
28 "unsupported encoding {name:?}; use utf-8, utf-16, utf-16le or utf-16be"
29 )),
30 }
31 }
32}
33
34fn invalid(message: &str) -> Error {
35 Error::new(ErrorKind::InvalidData, message)
36}
37
38pub fn has_utf16_bom(bytes: &[u8]) -> bool {
40 !bytes.starts_with(&[0xff, 0xfe, 0, 0])
41 && (bytes.starts_with(&[0xff, 0xfe]) || bytes.starts_with(&[0xfe, 0xff]))
42}
43
44impl Encoding {
45 pub fn decode(self, bytes: &[u8]) -> Result<String> {
51 if self == Self::Utf8 {
52 let bytes = bytes.strip_prefix(&[0xef, 0xbb, 0xbf]).unwrap_or(bytes);
53 return std::str::from_utf8(bytes)
54 .map(str::to_owned)
55 .map_err(|_| invalid("input is not valid UTF-8"));
56 }
57 if self == Self::Utf16
58 && (bytes.starts_with(&[0xff, 0xfe, 0, 0]) || bytes.starts_with(&[0, 0, 0xfe, 0xff]))
59 {
60 return Err(invalid(
61 "UTF-32 is not supported; convert the input to UTF-8",
62 ));
63 }
64 let bom = if bytes.starts_with(&[0xff, 0xfe]) {
65 Some(Self::Utf16Le)
66 } else if bytes.starts_with(&[0xfe, 0xff]) {
67 Some(Self::Utf16Be)
68 } else {
69 None
70 };
71 let endian = match (self, bom) {
72 (Self::Utf16, None) => {
73 return Err(invalid(
74 "UTF-16 needs a BOM; select --encoding utf-16le or utf-16be for headerless input",
75 ));
76 }
77 (Self::Utf16, Some(endian)) => endian,
78 (selected, Some(endian)) if selected != endian => {
79 return Err(invalid("UTF-16 BOM conflicts with the selected encoding"));
80 }
81 (selected, _) => selected,
82 };
83 let bytes = if bom.is_some() { &bytes[2..] } else { bytes };
84 if bytes.len() % 2 != 0 {
85 return Err(invalid("invalid UTF-16: odd byte count"));
86 }
87 let units = bytes.as_chunks::<2>().0.iter().map(|&pair| {
88 if endian == Self::Utf16Le {
89 u16::from_le_bytes(pair)
90 } else {
91 u16::from_be_bytes(pair)
92 }
93 });
94 char::decode_utf16(units)
95 .collect::<std::result::Result<String, _>>()
96 .map_err(|_| invalid("invalid UTF-16: unpaired surrogate"))
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn explicit_endianness_preserves_unicode() {
106 let text = "Hello 漢字 🙂\r\n";
107 for encoding in [Encoding::Utf16Le, Encoding::Utf16Be] {
108 let mut bytes: Vec<u8> = text
109 .encode_utf16()
110 .flat_map(|unit| {
111 if encoding == Encoding::Utf16Le {
112 unit.to_le_bytes()
113 } else {
114 unit.to_be_bytes()
115 }
116 })
117 .collect();
118 assert_eq!(encoding.decode(&bytes).unwrap(), text);
119 assert!(Encoding::Utf16.decode(&bytes).is_err());
120 let bom = if encoding == Encoding::Utf16Le {
121 [0xff, 0xfe]
122 } else {
123 [0xfe, 0xff]
124 };
125 bytes.splice(0..0, bom);
126 assert_eq!(Encoding::Utf16.decode(&bytes).unwrap(), text);
127 assert_eq!(encoding.decode(&bytes).unwrap(), text);
128 }
129 }
130
131 #[test]
132 fn malformed_and_conflicting_input_is_rejected() {
133 assert!(Encoding::Utf16Le.decode(&[1]).is_err());
134 assert!(Encoding::Utf16Le.decode(&[0, 0xd8]).is_err());
135 assert!(Encoding::Utf16Be.decode(&[0xff, 0xfe, 65, 0]).is_err());
136 assert!(Encoding::Utf16.decode(&[0xff, 0xfe, 0, 0]).is_err());
137 assert!(!has_utf16_bom(&[0xff, 0xfe, 0, 0]));
138 assert_eq!(Encoding::Utf16Le.decode(&[0xff, 0xfe, 0, 0]).unwrap(), "\0");
139 assert!(Encoding::Utf8.decode(&[0xff]).is_err());
140 assert_eq!(
141 Encoding::Utf8.decode(b"\xef\xbb\xbfhello").unwrap(),
142 "hello"
143 );
144 }
145}