1mod cjk;
34mod iso2022;
35mod single_byte;
36mod utf;
37
38use std::collections::HashMap;
39use std::fmt;
40use std::sync::OnceLock;
41
42use rustc_hash::FxHashMap;
43
44use crate::tables::{SINGLE_BYTE_CODECS, iana_lookup};
45
46use cjk::{CJK_NAMES, cjk, decode_cjk};
47use iso2022::{decode_hz, decode_iso2022};
48use single_byte::{decode_ascii, decode_single_byte, single_byte_table};
49use utf::{decode_utf7, decode_utf8, decode_utf16, decode_utf32};
50
51#[derive(Clone, Copy, PartialEq, Eq, Debug)]
53pub enum Errors {
54 Strict,
56 Ignore,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum DecodeError {
63 Unknown,
65 Invalid,
67}
68
69impl fmt::Display for DecodeError {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 DecodeError::Unknown => f.write_str("unknown encoding"),
73 DecodeError::Invalid => f.write_str("payload is not valid for the encoding"),
74 }
75 }
76}
77
78impl std::error::Error for DecodeError {}
79
80#[derive(Clone, Copy, PartialEq, Eq, Debug)]
81enum Endian {
82 Little,
83 Big,
84 Detect,
85}
86
87#[derive(Clone, Copy)]
88enum Iso2022Variant {
89 Kr,
90 Jp,
91 Jp1,
92 Jp2,
93 Jp2004,
94 Jp3,
95 JpExt,
96}
97
98#[derive(Clone, Copy)]
99enum Codec {
100 Ascii,
101 Latin1,
102 SingleByte(usize),
104 Utf8 {
105 sig: bool,
106 },
107 Utf16(Endian),
108 Utf32(Endian),
109 Utf7,
110 Cjk(usize),
112 Iso2022(Iso2022Variant),
113 Hz,
114}
115
116const PROGRAMMATIC_NAMES: [&str; 19] = [
118 "ascii",
119 "latin_1",
120 "utf_8",
121 "utf_8_sig",
122 "utf_16",
123 "utf_16_le",
124 "utf_16_be",
125 "utf_32",
126 "utf_32_le",
127 "utf_32_be",
128 "utf_7",
129 "hz",
130 "iso2022_kr",
131 "iso2022_jp",
132 "iso2022_jp_1",
133 "iso2022_jp_2",
134 "iso2022_jp_2004",
135 "iso2022_jp_3",
136 "iso2022_jp_ext",
137];
138
139fn codec_by_name(name: &str) -> Option<Codec> {
142 static CODECS: OnceLock<FxHashMap<&'static str, Codec>> = OnceLock::new();
143 CODECS
144 .get_or_init(|| {
145 PROGRAMMATIC_NAMES
146 .iter()
147 .chain(&CJK_NAMES)
148 .copied()
149 .chain(SINGLE_BYTE_CODECS.iter().map(|(name, _)| *name))
150 .filter_map(|name| Some((name, resolve_codec(name)?)))
151 .collect()
152 })
153 .get(name)
154 .copied()
155}
156
157fn resolve_codec(name: &str) -> Option<Codec> {
158 Some(match name {
159 "ascii" => Codec::Ascii,
160 "latin_1" => Codec::Latin1,
161 "utf_8" => Codec::Utf8 { sig: false },
162 "utf_8_sig" => Codec::Utf8 { sig: true },
163 "utf_16" => Codec::Utf16(Endian::Detect),
164 "utf_16_le" => Codec::Utf16(Endian::Little),
165 "utf_16_be" => Codec::Utf16(Endian::Big),
166 "utf_32" => Codec::Utf32(Endian::Detect),
167 "utf_32_le" => Codec::Utf32(Endian::Little),
168 "utf_32_be" => Codec::Utf32(Endian::Big),
169 "utf_7" => Codec::Utf7,
170 "hz" => Codec::Hz,
171 "iso2022_kr" => Codec::Iso2022(Iso2022Variant::Kr),
172 "iso2022_jp" => Codec::Iso2022(Iso2022Variant::Jp),
173 "iso2022_jp_1" => Codec::Iso2022(Iso2022Variant::Jp1),
174 "iso2022_jp_2" => Codec::Iso2022(Iso2022Variant::Jp2),
175 "iso2022_jp_2004" => Codec::Iso2022(Iso2022Variant::Jp2004),
176 "iso2022_jp_3" => Codec::Iso2022(Iso2022Variant::Jp3),
177 "iso2022_jp_ext" => Codec::Iso2022(Iso2022Variant::JpExt),
178 "big5" | "big5hkscs" | "cp932" | "cp949" | "cp950" | "euc_jis_2004" | "euc_jisx0213"
179 | "euc_jp" | "euc_kr" | "gb18030" | "gb2312" | "gbk" | "johab" | "shift_jis"
180 | "shift_jis_2004" | "shift_jisx0213" => {
181 Codec::Cjk(CJK_NAMES.iter().position(|value| *value == name)?)
182 }
183 _ => {
184 let index = SINGLE_BYTE_CODECS
185 .binary_search_by(|(codec, _)| (*codec).cmp(name))
186 .ok()?;
187 Codec::SingleByte(index)
188 }
189 })
190}
191
192fn lookup(encoding: &str) -> Option<Codec> {
194 if let Some(codec) = codec_by_name(encoding) {
195 return Some(codec);
196 }
197 let mut normalized = String::with_capacity(encoding.len());
198 let mut punctuation = false;
199 for character in encoding.chars() {
200 if character.is_alphanumeric() || character == '.' {
201 if punctuation && !normalized.is_empty() {
202 normalized.push('_');
203 }
204 normalized.push(character.to_ascii_lowercase());
205 punctuation = false;
206 } else {
207 punctuation = true;
208 }
209 }
210 codec_by_name(&normalized).or_else(|| codec_by_name(iana_lookup(&normalized)?))
211}
212
213#[must_use]
215pub fn is_known(encoding: &str) -> bool {
216 lookup(encoding).is_some()
217}
218
219pub(crate) fn single_byte_name(encoding: &str) -> Option<&'static str> {
222 match lookup(encoding)? {
223 Codec::Ascii => Some("ascii"),
224 Codec::Latin1 => Some("latin_1"),
225 Codec::SingleByte(index) => Some(SINGLE_BYTE_CODECS[index].0),
226 _ => None,
227 }
228}
229
230pub(crate) fn single_byte_names() -> impl Iterator<Item = &'static str> {
232 ["ascii", "latin_1"]
233 .into_iter()
234 .chain(SINGLE_BYTE_CODECS.iter().map(|(name, _)| *name))
235}
236
237pub fn decode(data: &[u8], encoding: &str, errors: Errors) -> Result<String, DecodeError> {
244 let mut out = String::new();
245 decode_into(data, encoding, errors, &mut out)?;
246 Ok(out)
247}
248
249#[cfg_attr(feature = "hotpath", hotpath::measure)]
256pub fn decode_into(
257 data: &[u8],
258 encoding: &str,
259 errors: Errors,
260 out: &mut String,
261) -> Result<(), DecodeError> {
262 let codec = lookup(encoding).ok_or(DecodeError::Unknown)?;
263 out.clear();
264 match codec {
265 Codec::Ascii => decode_ascii(data, errors, out),
266 Codec::Latin1 => {
267 out.extend(data.iter().map(|&byte| char::from(byte)));
268 Ok(())
269 }
270 Codec::SingleByte(index) => decode_single_byte(data, index, errors, out),
271 Codec::Utf8 { sig } => {
272 let data = if sig {
273 data.strip_prefix(b"\xef\xbb\xbf").unwrap_or(data)
274 } else {
275 data
276 };
277 decode_utf8(data, errors, out)
278 }
279 Codec::Utf16(endian) => decode_utf16(data, endian, errors, out),
280 Codec::Utf32(endian) => decode_utf32(data, endian, errors, out),
281 Codec::Utf7 => decode_utf7(data, errors, out),
282 Codec::Cjk(index) => decode_cjk(cjk().codec(index), data, errors, out),
283 Codec::Iso2022(variant) => decode_iso2022(variant, data, errors, out),
284 Codec::Hz => decode_hz(data, errors, out),
285 }
286}
287
288pub fn is_valid(data: &[u8], encoding: &str) -> Result<bool, DecodeError> {
295 match lookup(encoding).ok_or(DecodeError::Unknown)? {
296 Codec::Ascii => Ok(data.is_ascii()),
297 Codec::Latin1 => Ok(true),
298 Codec::SingleByte(index) => Ok(single_byte_table(index).is_valid(data)),
299 Codec::Utf8 { .. } => Ok(std::str::from_utf8(data).is_ok()),
300 _ => match decode(data, encoding, Errors::Strict) {
301 Ok(_) => Ok(true),
302 Err(DecodeError::Invalid) => Ok(false),
303 Err(error) => Err(error),
304 },
305 }
306}
307
308#[must_use]
311pub fn single_byte_decoder(encoding: &str) -> Option<impl Fn(u8) -> Option<char> + use<>> {
312 let codec = lookup(encoding)?;
313 let chars = match codec {
314 Codec::Ascii | Codec::Latin1 => None,
315 Codec::SingleByte(index) => Some(&single_byte_table(index).chars),
316 _ => return None,
317 };
318 let ascii = matches!(codec, Codec::Ascii);
319 Some(move |byte: u8| match chars {
320 Some(chars) => chars[byte as usize],
321 None if ascii => (byte < 0x80).then_some(byte as char),
322 None => Some(byte as char),
323 })
324}
325
326#[must_use]
328pub fn encode(text: &str, encoding: &str) -> Option<Vec<u8>> {
329 match lookup(encoding)? {
330 Codec::Utf8 { sig } => {
331 let mut out = Vec::with_capacity(text.len() + 3);
332 if sig {
333 out.extend_from_slice(b"\xef\xbb\xbf");
334 }
335 out.extend_from_slice(text.as_bytes());
336 Some(out)
337 }
338 Codec::Utf16(endian) => {
339 let mut out = Vec::with_capacity(text.len() * 2 + 2);
340 let little = endian != Endian::Big;
341 if endian == Endian::Detect {
342 out.extend_from_slice(b"\xff\xfe");
343 }
344 for unit in text.encode_utf16() {
345 out.extend_from_slice(&if little {
346 unit.to_le_bytes()
347 } else {
348 unit.to_be_bytes()
349 });
350 }
351 Some(out)
352 }
353 Codec::Utf32(endian) => {
354 let mut out = Vec::with_capacity(text.len() * 4 + 4);
355 let little = endian != Endian::Big;
356 if endian == Endian::Detect {
357 out.extend_from_slice(b"\xff\xfe\x00\x00");
358 }
359 for character in text.chars() {
360 let value = character as u32;
361 out.extend_from_slice(&if little {
362 value.to_le_bytes()
363 } else {
364 value.to_be_bytes()
365 });
366 }
367 Some(out)
368 }
369 Codec::Ascii => Some(
370 text.chars()
371 .map(|c| if c.is_ascii() { c as u8 } else { b'?' })
372 .collect(),
373 ),
374 Codec::Latin1 => Some(
375 text.chars()
376 .map(|c| if (c as u32) < 256 { c as u8 } else { b'?' })
377 .collect(),
378 ),
379 Codec::SingleByte(index) => {
380 let table = &SINGLE_BYTE_CODECS[index].1;
381 let mut reverse = HashMap::with_capacity(256);
383 for (byte, &value) in (0u8..=255).zip(table.iter()) {
384 if value != 0xFFFE {
385 reverse.insert(u32::from(value), byte);
386 }
387 }
388 let question = *reverse.get(&('?' as u32))?;
389 Some(
390 text.chars()
391 .map(|c| reverse.get(&(c as u32)).copied().unwrap_or(question))
392 .collect(),
393 )
394 }
395 _ => None,
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn utf_family() {
405 assert_eq!(
406 decode(b"caf\xc3\xa9", "utf_8", Errors::Strict).unwrap(),
407 "café"
408 );
409 assert!(decode(b"\xc3", "utf_8", Errors::Strict).is_err());
410 assert_eq!(decode(b"a\xffb", "utf_8", Errors::Ignore).unwrap(), "ab");
411 assert_eq!(
412 decode(b"\xff\xfea\x00", "utf_16", Errors::Strict).unwrap(),
413 "a"
414 );
415 assert_eq!(decode(b"\x00a", "utf_16_be", Errors::Strict).unwrap(), "a");
416 assert_eq!(
417 decode(b"+AGEAYgBj-", "utf_7", Errors::Strict).unwrap(),
418 "abc"
419 );
420 }
421
422 #[test]
423 fn cjk_codecs() {
424 assert_eq!(decode(b"\xa4\xa4", "big5", Errors::Strict).unwrap(), "中");
425 assert_eq!(
426 decode(b"\x1b$B%F%9%H\x1b(B", "iso2022_jp", Errors::Strict).unwrap(),
427 "テスト"
428 );
429 assert_eq!(decode(b"~{VP~}", "hz", Errors::Strict).unwrap(), "中");
430 assert_eq!(
431 decode(b"\x81\x30\x81\x30", "gb18030", Errors::Strict).unwrap(),
432 "\u{80}"
433 );
434 assert!(decode(b"\xa4", "big5", Errors::Strict).is_err());
435 }
436
437 #[test]
438 fn aliases_resolve() {
439 assert!(is_known("UTF-8"));
440 assert!(is_known("latin-1"));
441 assert!(is_known("windows-1252"));
442 assert!(!is_known("not-a-codec"));
443 }
444}