Skip to main content

charset_norm/codecs/
mod.rs

1//! Native implementations of the `CPython` codecs charset-normalizer probes.
2//!
3//! Decoding must match `CPython` bit for bit: the detector's verdict depends on
4//! which byte sequences a codec rejects. Unicode transformation formats are
5//! implemented directly, single-byte code pages and CJK codecs are driven by
6//! tables generated from `CPython` (see `bin/generate_native_tables.py`), and
7//! the stateful ISO-2022 / HZ / UTF-7 decoders are ports of `CPython`'s.
8//!
9//! Codec names follow `CPython` (`"cp1252"`, `"shift_jis"`, ...); aliases such
10//! as `"windows-1252"` or `"UTF-8"` are accepted too.
11//!
12//! ```
13//! use charset_norm::codecs::{decode, encode, Errors};
14//!
15//! let bytes = encode("Grüße", "cp1252").unwrap();
16//! assert_eq!(bytes, b"Gr\xfc\xdfe");
17//! assert_eq!(decode(&bytes, "windows-1252", Errors::Strict).unwrap(), "Grüße");
18//! ```
19//!
20//! Invalid input either fails ([`Errors::Strict`]) or is skipped
21//! ([`Errors::Ignore`]):
22//!
23//! ```
24//! use charset_norm::codecs::{DecodeError, Errors, decode};
25//!
26//! assert_eq!(decode(b"caf\xe9", "utf-8", Errors::Strict), Err(DecodeError::Invalid));
27//! assert_eq!(decode(b"caf\xe9", "utf-8", Errors::Ignore).unwrap(), "caf");
28//! ```
29//!
30//! Every name in [`encoding::supported_encodings`](crate::encoding::supported_encodings)
31//! has a decoder here; [`is_known`] checks any other name.
32
33mod 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/// How decoding handles invalid input.
52#[derive(Clone, Copy, PartialEq, Eq, Debug)]
53pub enum Errors {
54    /// Fail on the first invalid sequence.
55    Strict,
56    /// Skip invalid sequences, as `CPython`'s `errors="ignore"` does.
57    Ignore,
58}
59
60/// Why decoding failed.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum DecodeError {
63    /// No native implementation for this codec name.
64    Unknown,
65    /// The payload is not valid for the codec (strict mode).
66    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    /// Index into `SINGLE_BYTE_CODECS`.
103    SingleByte(usize),
104    Utf8 {
105        sig: bool,
106    },
107    Utf16(Endian),
108    Utf32(Endian),
109    Utf7,
110    /// Index into `CJK_NAMES`.
111    Cjk(usize),
112    Iso2022(Iso2022Variant),
113    Hz,
114}
115
116/// Canonical names of the codecs that are not table-driven.
117const 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
139/// The codec of a canonical name. The detector resolves names for every
140/// chunk it decodes, so they are looked up in a hash map built once.
141fn 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
192/// `encodings.normalize_encoding` followed by the alias table.
193fn 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/// Whether a native codec exists for `encoding` (or one of its aliases).
214#[must_use]
215pub fn is_known(encoding: &str) -> bool {
216    lookup(encoding).is_some()
217}
218
219/// Canonical name of a single-byte code page (`ascii`, `latin_1` or a table),
220/// resolving aliases; `None` for other codecs.
221pub(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
230/// Canonical names of every single-byte code page with a native codec.
231pub(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
237/// Decode `data` with `encoding`, exactly as `CPython`'s `bytes.decode` would.
238///
239/// # Errors
240///
241/// [`DecodeError::Invalid`] when `data` is not valid for the encoding (strict
242/// mode), [`DecodeError::Unknown`] when the encoding has no native codec.
243pub 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/// [`decode`] into a reusable buffer: `out` is cleared first, and its
250/// contents are unspecified when an error is returned.
251///
252/// # Errors
253///
254/// As for [`decode`].
255#[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
288/// Whether `decode(data, encoding, Strict)` would succeed, without building
289/// the text when the codec allows a cheaper check.
290///
291/// # Errors
292///
293/// [`DecodeError::Unknown`] when the encoding has no native codec.
294pub 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/// Decode one byte in isolation, as `IncrementalDecoder(errors="ignore")`
309/// does for single-byte code pages. `None` for multi-byte codecs.
310#[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/// Encode with `errors="replace"`. `None` when there is no native encoder.
327#[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            // Later bytes win on duplicates, like codecs.charmap_build.
382            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}