Skip to main content

oxixml_encoding/
label.rs

1//! The [`Encoding`] enum and IANA/WHATWG label resolution.
2
3use crate::decoder::Decoder;
4
5/// A character encoding recognized by this crate.
6///
7/// The original core set is exactly what an XML processor needs to deal with in practice:
8/// `UTF-8` and `UTF-16` (in both byte orders) are the two encodings XML 1.0 requires every
9/// conformant processor to support; [`Encoding::Latin1`] and [`Encoding::Windows1252`] cover the
10/// overwhelming majority of legacy (`encoding="..."`) documents seen in the wild, and
11/// [`Encoding::Ascii`] is useful as a strict validation target.
12///
13/// Beyond that core, this crate also implements the rest of the
14/// [WHATWG Encoding Standard](https://encoding.spec.whatwg.org/)'s legacy single-byte encodings
15/// (the full `ISO-8859-*`, `windows-125x`, `KOI8-*`, `IBM866`, `macintosh` and `x-mac-cyrillic`
16/// families) and its three legacy Japanese multi-byte encodings (Shift_JIS, EUC-JP, and
17/// ISO-2022-JP), prioritized for COOLJAPAN. See [`Encoding::ALL`] for the complete list.
18///
19/// Unlike the WHATWG Encoding Standard, which folds `ISO-8859-1` into `windows-1252` for web
20/// compatibility, this crate keeps them distinct: XML tooling (libxml2, Xerces, .NET, the JDK,
21/// ...) has always treated `ISO-8859-1` as the "pure" Latin-1 repertoire (bytes `0x00..=0xFF` map
22/// to `U+0000..=U+00FF` unchanged) and `windows-1252` as the Microsoft superset that additionally
23/// assigns the `0x80..=0x9F` range to punctuation such as smart quotes and the euro sign. The
24/// same reasoning applies to [`Encoding::Iso8859_9`] versus [`Encoding::Windows1254`]: the WHATWG
25/// standard folds the `"iso-8859-9"`/`"latin5"` labels into windows-1254 too, but this crate keeps
26/// pure ISO-8859-9 (ISO/IEC 8859-9:1999, a.k.a. Latin-5) as its own encoding for the same reason.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Encoding {
29    /// UTF-8: the ASCII-compatible variable-width encoding of Unicode, and the default assumed
30    /// by XML 1.0 in the absence of a byte-order mark or `encoding` declaration.
31    Utf8,
32    /// UTF-16, little-endian byte order.
33    Utf16Le,
34    /// UTF-16, big-endian byte order.
35    Utf16Be,
36    /// ISO-8859-1 (Latin-1): each byte `b` decodes to the scalar value `U+00xx` with `xx == b`.
37    Latin1,
38    /// windows-1252 (a.k.a. `cp1252`): ISO-8859-1 with the C1 control range `0x80..=0x9F`
39    /// reassigned to printable characters (the five positions Microsoft left unassigned keep
40    /// their Latin-1 fallback, per the [WHATWG index table][idx]).
41    ///
42    /// [idx]: https://encoding.spec.whatwg.org/index-windows-1252.txt
43    Windows1252,
44    /// US-ASCII: the 7-bit repertoire `U+0000..=U+007F`. Bytes `0x80..=0xFF` are not valid.
45    Ascii,
46
47    // --- ISO-8859 family (beyond -1, above) -------------------------------------------------
48    /// ISO-8859-2 (Latin-2): Central/Eastern European.
49    Iso8859_2,
50    /// ISO-8859-3 (Latin-3): South European (Maltese, Esperanto).
51    Iso8859_3,
52    /// ISO-8859-4 (Latin-4): North European (Baltic languages, pre-ISO-8859-13).
53    Iso8859_4,
54    /// ISO-8859-5: Cyrillic.
55    Iso8859_5,
56    /// ISO-8859-6: Arabic.
57    Iso8859_6,
58    /// ISO-8859-7: Greek.
59    Iso8859_7,
60    /// ISO-8859-8: Hebrew (visual order; this crate makes no distinction from the "logical
61    /// order" `ISO-8859-8-I` variant, since directionality is outside this crate's scope and
62    /// both share the same byte/code-point repertoire).
63    Iso8859_8,
64    /// ISO-8859-9 (Latin-5): identical to ISO-8859-1 except six positions replaced with the
65    /// Turkish letters `Ğ`/`ğ`, `İ`/`ı`, `Ş`/`ş`. See the type-level docs for why this is kept
66    /// distinct from [`Encoding::Windows1254`].
67    Iso8859_9,
68    /// ISO-8859-10 (Latin-6): Nordic (an alternative to -4 with full Icelandic coverage).
69    Iso8859_10,
70    /// ISO-8859-13 (Latin-7): Baltic.
71    Iso8859_13,
72    /// ISO-8859-14 (Latin-8): Celtic.
73    Iso8859_14,
74    /// ISO-8859-15 (Latin-9): Latin-1 with the euro sign and a handful of French/Finnish letters.
75    Iso8859_15,
76    /// ISO-8859-16 (Latin-10): South-Eastern European.
77    Iso8859_16,
78
79    // --- windows-125x family (beyond -1252, above) ------------------------------------------
80    /// windows-1250: Central/Eastern European.
81    Windows1250,
82    /// windows-1251: Cyrillic.
83    Windows1251,
84    /// windows-1253: Greek.
85    Windows1253,
86    /// windows-1254 (Turkish): the Microsoft superset of [`Encoding::Iso8859_9`].
87    Windows1254,
88    /// windows-1255: Hebrew.
89    Windows1255,
90    /// windows-1256: Arabic.
91    Windows1256,
92    /// windows-1257: Baltic.
93    Windows1257,
94    /// windows-1258: Vietnamese.
95    Windows1258,
96
97    // --- Other single-byte encodings ---------------------------------------------------------
98    /// KOI8-R: Russian Cyrillic.
99    Koi8R,
100    /// KOI8-U: Ukrainian Cyrillic (a KOI8-R variant with a handful of Ukrainian letters).
101    Koi8U,
102    /// IBM866 (code page 866): Cyrillic, MS-DOS.
103    Ibm866,
104    /// macintosh (a.k.a. "Mac OS Roman"): the classic Mac OS Western European encoding.
105    Macintosh,
106    /// x-mac-cyrillic (a.k.a. "Mac OS Cyrillic"): the classic Mac OS Cyrillic encoding.
107    XMacCyrillic,
108
109    // --- Legacy multi-byte Japanese encodings ------------------------------------------------
110    /// Shift_JIS (a.k.a. `MS932`/`Windows-31J`): the JIS X 0201 + JIS X 0208 encoding most
111    /// commonly used on Japanese Windows and in Japanese web content.
112    ShiftJis,
113    /// EUC-JP: the Unix-family Japanese encoding covering JIS X 0201, JIS X 0208, and (via its
114    /// `0x8F`-prefixed `SS3` sequences) JIS X 0212.
115    EucJp,
116    /// ISO-2022-JP: the 7-bit, escape-sequence-switched Japanese encoding historically used for
117    /// Japanese email (`Content-Transfer-Encoding: 7bit`-safe).
118    Iso2022Jp,
119}
120
121impl Encoding {
122    /// Every encoding this crate supports, in declaration order. Useful for exhaustively testing
123    /// or listing supported encodings without having to enumerate the (growing) variant list by
124    /// hand.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use oxixml_encoding::Encoding;
130    ///
131    /// assert!(Encoding::ALL.contains(&Encoding::Utf8));
132    /// assert!(Encoding::ALL.contains(&Encoding::ShiftJis));
133    /// // Every entry's canonical name resolves back to itself.
134    /// for &encoding in Encoding::ALL {
135    ///     assert_eq!(Encoding::for_label(encoding.name()), Some(encoding));
136    /// }
137    /// ```
138    pub const ALL: &'static [Encoding] = &[
139        Encoding::Utf8,
140        Encoding::Utf16Le,
141        Encoding::Utf16Be,
142        Encoding::Latin1,
143        Encoding::Windows1252,
144        Encoding::Ascii,
145        Encoding::Iso8859_2,
146        Encoding::Iso8859_3,
147        Encoding::Iso8859_4,
148        Encoding::Iso8859_5,
149        Encoding::Iso8859_6,
150        Encoding::Iso8859_7,
151        Encoding::Iso8859_8,
152        Encoding::Iso8859_9,
153        Encoding::Iso8859_10,
154        Encoding::Iso8859_13,
155        Encoding::Iso8859_14,
156        Encoding::Iso8859_15,
157        Encoding::Iso8859_16,
158        Encoding::Windows1250,
159        Encoding::Windows1251,
160        Encoding::Windows1253,
161        Encoding::Windows1254,
162        Encoding::Windows1255,
163        Encoding::Windows1256,
164        Encoding::Windows1257,
165        Encoding::Windows1258,
166        Encoding::Koi8R,
167        Encoding::Koi8U,
168        Encoding::Ibm866,
169        Encoding::Macintosh,
170        Encoding::XMacCyrillic,
171        Encoding::ShiftJis,
172        Encoding::EucJp,
173        Encoding::Iso2022Jp,
174    ];
175
176    /// Resolve an IANA/WHATWG charset label to an [`Encoding`], case-insensitively and after
177    /// trimming ASCII whitespace, mirroring the label tables in the
178    /// [WHATWG Encoding Standard](https://encoding.spec.whatwg.org/#names-and-labels) — with the
179    /// [`Encoding::Iso8859_9`]-versus-[`Encoding::Windows1254`] exception documented on
180    /// [`Encoding`] itself.
181    ///
182    /// Returns [`None`] for labels this crate does not resolve to a supported encoding (for
183    /// example `"utf-32"`, `"gb18030"` or `"ebcdic-cp-us"`).
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use oxixml_encoding::Encoding;
189    ///
190    /// assert_eq!(Encoding::for_label("UTF-8"), Some(Encoding::Utf8));
191    /// assert_eq!(Encoding::for_label(" utf8 \t"), Some(Encoding::Utf8));
192    /// assert_eq!(Encoding::for_label("Windows-1252"), Some(Encoding::Windows1252));
193    /// assert_eq!(Encoding::for_label("iso-8859-1"), Some(Encoding::Latin1));
194    /// assert_eq!(Encoding::for_label("Shift_JIS"), Some(Encoding::ShiftJis));
195    /// assert_eq!(Encoding::for_label("EUC-JP"), Some(Encoding::EucJp));
196    /// assert_eq!(Encoding::for_label("gb18030"), None);
197    /// ```
198    #[must_use]
199    pub fn for_label(label: &str) -> Option<Encoding> {
200        let trimmed = label.trim_matches(is_whatwg_ascii_whitespace);
201        if trimmed.is_empty() {
202            return None;
203        }
204        // WHATWG label matching is defined over the ASCII-lowercased label; avoid allocating in
205        // the common case where the caller already passed a canonical lowercase label.
206        if trimmed.bytes().all(|b| !b.is_ascii_uppercase()) {
207            return encoding_for_lowercase_label(trimmed);
208        }
209        let lowered = trimmed.to_ascii_lowercase();
210        encoding_for_lowercase_label(&lowered)
211    }
212
213    /// The canonical name of this encoding, as it should appear in generated `encoding="..."`
214    /// declarations or diagnostics.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use oxixml_encoding::Encoding;
220    ///
221    /// assert_eq!(Encoding::Utf8.name(), "UTF-8");
222    /// assert_eq!(Encoding::Windows1252.name(), "windows-1252");
223    /// assert_eq!(Encoding::ShiftJis.name(), "Shift_JIS");
224    /// ```
225    #[must_use]
226    pub const fn name(&self) -> &'static str {
227        match self {
228            Encoding::Utf8 => "UTF-8",
229            Encoding::Utf16Le => "UTF-16LE",
230            Encoding::Utf16Be => "UTF-16BE",
231            Encoding::Latin1 => "ISO-8859-1",
232            Encoding::Windows1252 => "windows-1252",
233            Encoding::Ascii => "US-ASCII",
234            Encoding::Iso8859_2 => "ISO-8859-2",
235            Encoding::Iso8859_3 => "ISO-8859-3",
236            Encoding::Iso8859_4 => "ISO-8859-4",
237            Encoding::Iso8859_5 => "ISO-8859-5",
238            Encoding::Iso8859_6 => "ISO-8859-6",
239            Encoding::Iso8859_7 => "ISO-8859-7",
240            Encoding::Iso8859_8 => "ISO-8859-8",
241            Encoding::Iso8859_9 => "ISO-8859-9",
242            Encoding::Iso8859_10 => "ISO-8859-10",
243            Encoding::Iso8859_13 => "ISO-8859-13",
244            Encoding::Iso8859_14 => "ISO-8859-14",
245            Encoding::Iso8859_15 => "ISO-8859-15",
246            Encoding::Iso8859_16 => "ISO-8859-16",
247            Encoding::Windows1250 => "windows-1250",
248            Encoding::Windows1251 => "windows-1251",
249            Encoding::Windows1253 => "windows-1253",
250            Encoding::Windows1254 => "windows-1254",
251            Encoding::Windows1255 => "windows-1255",
252            Encoding::Windows1256 => "windows-1256",
253            Encoding::Windows1257 => "windows-1257",
254            Encoding::Windows1258 => "windows-1258",
255            Encoding::Koi8R => "KOI8-R",
256            Encoding::Koi8U => "KOI8-U",
257            Encoding::Ibm866 => "IBM866",
258            Encoding::Macintosh => "macintosh",
259            Encoding::XMacCyrillic => "x-mac-cyrillic",
260            Encoding::ShiftJis => "Shift_JIS",
261            Encoding::EucJp => "EUC-JP",
262            Encoding::Iso2022Jp => "ISO-2022-JP",
263        }
264    }
265
266    /// Whether this encoding is ASCII-compatible in the sense relevant to XML prologue
267    /// scanning: every byte in `0x00..=0x7F` decodes to the identically-valued scalar, so an
268    /// ASCII-only substring (such as an `<?xml ... ?>` declaration) can be located by scanning
269    /// raw bytes without first performing a full decode.
270    ///
271    /// This holds for every single-byte encoding this crate supports (all of them agree with
272    /// ASCII below `0x80`, by construction — see [`Encoding::ALL`]), not only the original four.
273    /// It does *not* hold for the three multi-byte Japanese encodings: even though each of them
274    /// also encodes `0x00..=0x7F` as ASCII in their respective *initial* shift state (which is
275    /// enough to make [`crate::sniff_xml_encoding`]'s prologue scan work, since an `<?xml ...?>`
276    /// declaration is required to be the first thing in the entity and is itself pure ASCII), a
277    /// non-initial byte elsewhere in the document is not reliably ASCII-interpretable without
278    /// tracking shift state, so this method intentionally answers `false` for them.
279    #[must_use]
280    pub const fn is_ascii_compatible_single_byte(&self) -> bool {
281        matches!(
282            self,
283            Encoding::Utf8
284                | Encoding::Latin1
285                | Encoding::Windows1252
286                | Encoding::Ascii
287                | Encoding::Iso8859_2
288                | Encoding::Iso8859_3
289                | Encoding::Iso8859_4
290                | Encoding::Iso8859_5
291                | Encoding::Iso8859_6
292                | Encoding::Iso8859_7
293                | Encoding::Iso8859_8
294                | Encoding::Iso8859_9
295                | Encoding::Iso8859_10
296                | Encoding::Iso8859_13
297                | Encoding::Iso8859_14
298                | Encoding::Iso8859_15
299                | Encoding::Iso8859_16
300                | Encoding::Windows1250
301                | Encoding::Windows1251
302                | Encoding::Windows1253
303                | Encoding::Windows1254
304                | Encoding::Windows1255
305                | Encoding::Windows1256
306                | Encoding::Windows1257
307                | Encoding::Windows1258
308                | Encoding::Koi8R
309                | Encoding::Koi8U
310                | Encoding::Ibm866
311                | Encoding::Macintosh
312                | Encoding::XMacCyrillic
313        )
314    }
315
316    /// Whether this encoding is one of the 26 legacy single-byte encodings backed by a generated
317    /// 128-entry upper-half table in `crate::tables::legacy_single_byte` (every [`Encoding`]
318    /// variant except UTF-8/UTF-16LE/UTF-16BE, the original [`Encoding::Latin1`]/
319    /// [`Encoding::Windows1252`]/[`Encoding::Ascii`] trio, and the three multi-byte Japanese
320    /// encodings).
321    #[must_use]
322    pub(crate) const fn is_legacy_single_byte_table(&self) -> bool {
323        matches!(
324            self,
325            Encoding::Iso8859_2
326                | Encoding::Iso8859_3
327                | Encoding::Iso8859_4
328                | Encoding::Iso8859_5
329                | Encoding::Iso8859_6
330                | Encoding::Iso8859_7
331                | Encoding::Iso8859_8
332                | Encoding::Iso8859_9
333                | Encoding::Iso8859_10
334                | Encoding::Iso8859_13
335                | Encoding::Iso8859_14
336                | Encoding::Iso8859_15
337                | Encoding::Iso8859_16
338                | Encoding::Windows1250
339                | Encoding::Windows1251
340                | Encoding::Windows1253
341                | Encoding::Windows1254
342                | Encoding::Windows1255
343                | Encoding::Windows1256
344                | Encoding::Windows1257
345                | Encoding::Windows1258
346                | Encoding::Koi8R
347                | Encoding::Koi8U
348                | Encoding::Ibm866
349                | Encoding::Macintosh
350                | Encoding::XMacCyrillic
351        )
352    }
353
354    /// Create a new incremental [`Decoder`] for this encoding.
355    ///
356    /// # Examples
357    ///
358    /// ```
359    /// use oxixml_encoding::Encoding;
360    ///
361    /// let mut decoder = Encoding::Utf8.new_decoder();
362    /// let mut out = String::new();
363    /// decoder.decode_to_string(b"hello", &mut out, true).expect("valid utf-8");
364    /// assert_eq!(out, "hello");
365    /// ```
366    #[must_use]
367    pub const fn new_decoder(self) -> Decoder {
368        Decoder::new(self)
369    }
370}
371
372impl std::fmt::Display for Encoding {
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        f.write_str(self.name())
375    }
376}
377
378/// WHATWG's definition of "ASCII whitespace" used when trimming labels: tab, line feed, form
379/// feed, carriage return and space.
380const fn is_whatwg_ascii_whitespace(c: char) -> bool {
381    matches!(c, '\t' | '\n' | '\x0C' | '\r' | ' ')
382}
383
384/// Resolve an already-trimmed, already-lowercased label. Kept as a `match` over `&str` (rather
385/// than a hash map) since the label set is small, fixed at compile time, and this keeps the
386/// crate free of any external dependency.
387///
388/// Label lists are transcribed from the WHATWG Encoding Standard
389/// ([`§4.2 "get an encoding"`](https://encoding.spec.whatwg.org/#names-and-labels), data dated
390/// 2024-09-18), with one deliberate deviation: the `"iso-8859-9"`/`"latin5"`-family labels
391/// resolve to [`Encoding::Iso8859_9`] here rather than [`Encoding::Windows1254`] (see the
392/// type-level docs on [`Encoding`]).
393fn encoding_for_lowercase_label(label: &str) -> Option<Encoding> {
394    match label {
395        // --- UTF-8 --------------------------------------------------------------------------
396        "unicode-1-1-utf-8" | "unicode11utf8" | "unicode20utf8" | "utf-8" | "utf8"
397        | "x-unicode20utf8" => Some(Encoding::Utf8),
398
399        // --- UTF-16, unspecified/little-endian ----------------------------------------------
400        // Per the WHATWG Encoding Standard, a bare "utf-16" label (no endianness suffix) is
401        // resolved as UTF-16LE for compatibility with the (much more common) little-endian
402        // content that historically omitted the suffix.
403        "utf-16" | "utf-16le" | "utf16" | "utf16le" | "ucs-2" | "unicode" | "iso-10646-ucs-2"
404        | "csunicode" => Some(Encoding::Utf16Le),
405
406        // --- UTF-16, big-endian ----------------------------------------------------------------
407        "utf-16be" | "utf16be" | "unicodefffe" => Some(Encoding::Utf16Be),
408
409        // --- ISO-8859-1 / Latin-1 --------------------------------------------------------------
410        "iso-8859-1" | "iso-ir-100" | "iso8859-1" | "iso88591" | "iso_8859-1"
411        | "iso_8859-1:1987" | "l1" | "latin1" | "csisolatin1" | "8859-1" | "88591" => {
412            Some(Encoding::Latin1)
413        }
414
415        // --- windows-1252 ------------------------------------------------------------------------
416        "windows-1252" | "cp1252" | "x-cp1252" => Some(Encoding::Windows1252),
417
418        // --- US-ASCII --------------------------------------------------------------------------
419        "us-ascii" | "ascii" | "ansi_x3.4-1968" | "ansi_x3.4-1986" | "iso646-us" | "iso-ir-6"
420        | "us" | "ibm367" | "cp367" | "csascii" => Some(Encoding::Ascii),
421
422        // --- ISO-8859-2 --------------------------------------------------------------------------
423        "csisolatin2" | "iso-8859-2" | "iso-ir-101" | "iso8859-2" | "iso88592" | "iso_8859-2"
424        | "iso_8859-2:1987" | "l2" | "latin2" => Some(Encoding::Iso8859_2),
425
426        // --- ISO-8859-3 --------------------------------------------------------------------------
427        "csisolatin3" | "iso-8859-3" | "iso-ir-109" | "iso8859-3" | "iso88593" | "iso_8859-3"
428        | "iso_8859-3:1988" | "l3" | "latin3" => Some(Encoding::Iso8859_3),
429
430        // --- ISO-8859-4 --------------------------------------------------------------------------
431        "csisolatin4" | "iso-8859-4" | "iso-ir-110" | "iso8859-4" | "iso88594" | "iso_8859-4"
432        | "iso_8859-4:1988" | "l4" | "latin4" => Some(Encoding::Iso8859_4),
433
434        // --- ISO-8859-5 --------------------------------------------------------------------------
435        "csisolatincyrillic" | "cyrillic" | "iso-8859-5" | "iso-ir-144" | "iso8859-5"
436        | "iso88595" | "iso_8859-5" | "iso_8859-5:1988" => Some(Encoding::Iso8859_5),
437
438        // --- ISO-8859-6 --------------------------------------------------------------------------
439        "arabic" | "asmo-708" | "csiso88596e" | "csiso88596i" | "csisolatinarabic" | "ecma-114"
440        | "iso-8859-6" | "iso-8859-6-e" | "iso-8859-6-i" | "iso-ir-127" | "iso8859-6"
441        | "iso88596" | "iso_8859-6" | "iso_8859-6:1987" => Some(Encoding::Iso8859_6),
442
443        // --- ISO-8859-7 --------------------------------------------------------------------------
444        "csisolatingreek" | "ecma-118" | "elot_928" | "greek" | "greek8" | "iso-8859-7"
445        | "iso-ir-126" | "iso8859-7" | "iso88597" | "iso_8859-7" | "iso_8859-7:1987"
446        | "sun_eu_greek" => Some(Encoding::Iso8859_7),
447
448        // --- ISO-8859-8 (and ISO-8859-8-I: same repertoire, direction only, out of scope) -------
449        "csiso88598e" | "csisolatinhebrew" | "hebrew" | "iso-8859-8" | "iso-8859-8-e"
450        | "iso-ir-138" | "iso8859-8" | "iso88598" | "iso_8859-8" | "iso_8859-8:1988" | "visual"
451        | "csiso88598i" | "iso-8859-8-i" | "logical" => Some(Encoding::Iso8859_8),
452
453        // --- ISO-8859-9 (kept distinct from windows-1254; see `Encoding` docs) -----------------
454        "csisolatin5" | "iso-8859-9" | "iso-ir-148" | "iso8859-9" | "iso88599" | "iso_8859-9"
455        | "iso_8859-9:1989" | "l5" | "latin5" => Some(Encoding::Iso8859_9),
456
457        // --- ISO-8859-10 -------------------------------------------------------------------------
458        "csisolatin6" | "iso-8859-10" | "iso-ir-157" | "iso8859-10" | "iso885910" | "l6"
459        | "latin6" => Some(Encoding::Iso8859_10),
460
461        // --- ISO-8859-13 -------------------------------------------------------------------------
462        "iso-8859-13" | "iso8859-13" | "iso885913" => Some(Encoding::Iso8859_13),
463
464        // --- ISO-8859-14 -------------------------------------------------------------------------
465        "iso-8859-14" | "iso8859-14" | "iso885914" => Some(Encoding::Iso8859_14),
466
467        // --- ISO-8859-15 -------------------------------------------------------------------------
468        "csisolatin9" | "iso-8859-15" | "iso8859-15" | "iso885915" | "iso_8859-15" | "l9" => {
469            Some(Encoding::Iso8859_15)
470        }
471
472        // --- ISO-8859-16 -------------------------------------------------------------------------
473        "iso-8859-16" => Some(Encoding::Iso8859_16),
474
475        // --- windows-1250 ------------------------------------------------------------------------
476        "cp1250" | "windows-1250" | "x-cp1250" => Some(Encoding::Windows1250),
477
478        // --- windows-1251 ------------------------------------------------------------------------
479        "cp1251" | "windows-1251" | "x-cp1251" => Some(Encoding::Windows1251),
480
481        // --- windows-1253 ------------------------------------------------------------------------
482        "cp1253" | "windows-1253" | "x-cp1253" => Some(Encoding::Windows1253),
483
484        // --- windows-1254 ------------------------------------------------------------------------
485        "cp1254" | "windows-1254" | "x-cp1254" => Some(Encoding::Windows1254),
486
487        // --- windows-1255 ------------------------------------------------------------------------
488        "cp1255" | "windows-1255" | "x-cp1255" => Some(Encoding::Windows1255),
489
490        // --- windows-1256 ------------------------------------------------------------------------
491        "cp1256" | "windows-1256" | "x-cp1256" => Some(Encoding::Windows1256),
492
493        // --- windows-1257 ------------------------------------------------------------------------
494        "cp1257" | "windows-1257" | "x-cp1257" => Some(Encoding::Windows1257),
495
496        // --- windows-1258 ------------------------------------------------------------------------
497        "cp1258" | "windows-1258" | "x-cp1258" => Some(Encoding::Windows1258),
498
499        // --- KOI8-R ------------------------------------------------------------------------------
500        "cskoi8r" | "koi" | "koi8" | "koi8-r" | "koi8_r" => Some(Encoding::Koi8R),
501
502        // --- KOI8-U ------------------------------------------------------------------------------
503        "koi8-ru" | "koi8-u" => Some(Encoding::Koi8U),
504
505        // --- IBM866 ------------------------------------------------------------------------------
506        "866" | "cp866" | "csibm866" | "ibm866" => Some(Encoding::Ibm866),
507
508        // --- macintosh ---------------------------------------------------------------------------
509        "csmacintosh" | "mac" | "macintosh" | "x-mac-roman" => Some(Encoding::Macintosh),
510
511        // --- x-mac-cyrillic ----------------------------------------------------------------------
512        "x-mac-cyrillic" | "x-mac-ukrainian" => Some(Encoding::XMacCyrillic),
513
514        // --- EUC-JP ------------------------------------------------------------------------------
515        "cseucpkdfmtjapanese" | "euc-jp" | "x-euc-jp" => Some(Encoding::EucJp),
516
517        // --- ISO-2022-JP -------------------------------------------------------------------------
518        "csiso2022jp" | "iso-2022-jp" => Some(Encoding::Iso2022Jp),
519
520        // --- Shift_JIS ---------------------------------------------------------------------------
521        "csshiftjis" | "ms932" | "ms_kanji" | "shift-jis" | "shift_jis" | "sjis"
522        | "windows-31j" | "x-sjis" => Some(Encoding::ShiftJis),
523
524        _ => None,
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    #[test]
533    fn for_label_is_case_insensitive() {
534        assert_eq!(Encoding::for_label("UTF-8"), Some(Encoding::Utf8));
535        assert_eq!(Encoding::for_label("Utf-8"), Some(Encoding::Utf8));
536        assert_eq!(Encoding::for_label("uTf8"), Some(Encoding::Utf8));
537        assert_eq!(
538            Encoding::for_label("WINDOWS-1252"),
539            Some(Encoding::Windows1252)
540        );
541        assert_eq!(Encoding::for_label("Iso-8859-1"), Some(Encoding::Latin1));
542        assert_eq!(Encoding::for_label("US-ASCII"), Some(Encoding::Ascii));
543        assert_eq!(Encoding::for_label("Shift_JIS"), Some(Encoding::ShiftJis));
544        assert_eq!(Encoding::for_label("SHIFT_JIS"), Some(Encoding::ShiftJis));
545    }
546
547    #[test]
548    fn for_label_trims_whatwg_whitespace() {
549        assert_eq!(Encoding::for_label("  utf-8\t\n"), Some(Encoding::Utf8));
550        assert_eq!(Encoding::for_label("\r\nlatin1 "), Some(Encoding::Latin1));
551        assert_eq!(
552            Encoding::for_label(" shift_jis \n"),
553            Some(Encoding::ShiftJis)
554        );
555    }
556
557    #[test]
558    fn for_label_unknown_returns_none() {
559        assert_eq!(Encoding::for_label("utf-32"), None);
560        assert_eq!(Encoding::for_label(""), None);
561        assert_eq!(Encoding::for_label("   "), None);
562        // Deliberately unsupported (cut from this pass; see crate changelog/report).
563        assert_eq!(Encoding::for_label("gbk"), None);
564        assert_eq!(Encoding::for_label("gb18030"), None);
565        assert_eq!(Encoding::for_label("big5"), None);
566        assert_eq!(Encoding::for_label("euc-kr"), None);
567        assert_eq!(Encoding::for_label("windows-874"), None);
568        assert_eq!(Encoding::for_label("iso-8859-11"), None);
569    }
570
571    #[test]
572    fn for_label_utf16_variants() {
573        assert_eq!(Encoding::for_label("utf-16"), Some(Encoding::Utf16Le));
574        assert_eq!(Encoding::for_label("utf-16le"), Some(Encoding::Utf16Le));
575        assert_eq!(Encoding::for_label("utf-16be"), Some(Encoding::Utf16Be));
576        assert_eq!(Encoding::for_label("unicodeFFFE"), Some(Encoding::Utf16Be));
577    }
578
579    #[test]
580    fn for_label_iso_8859_9_is_kept_distinct_from_windows_1254() {
581        assert_eq!(Encoding::for_label("iso-8859-9"), Some(Encoding::Iso8859_9));
582        assert_eq!(Encoding::for_label("latin5"), Some(Encoding::Iso8859_9));
583        assert_eq!(
584            Encoding::for_label("windows-1254"),
585            Some(Encoding::Windows1254)
586        );
587        assert_eq!(Encoding::for_label("cp1254"), Some(Encoding::Windows1254));
588        assert_ne!(Encoding::Iso8859_9, Encoding::Windows1254);
589    }
590
591    #[test]
592    fn for_label_japanese_multi_byte_labels() {
593        assert_eq!(Encoding::for_label("shift_jis"), Some(Encoding::ShiftJis));
594        assert_eq!(Encoding::for_label("sjis"), Some(Encoding::ShiftJis));
595        assert_eq!(Encoding::for_label("ms932"), Some(Encoding::ShiftJis));
596        assert_eq!(Encoding::for_label("windows-31j"), Some(Encoding::ShiftJis));
597        assert_eq!(Encoding::for_label("euc-jp"), Some(Encoding::EucJp));
598        assert_eq!(Encoding::for_label("x-euc-jp"), Some(Encoding::EucJp));
599        assert_eq!(
600            Encoding::for_label("iso-2022-jp"),
601            Some(Encoding::Iso2022Jp)
602        );
603        assert_eq!(
604            Encoding::for_label("csiso2022jp"),
605            Some(Encoding::Iso2022Jp)
606        );
607    }
608
609    #[test]
610    fn name_round_trips_through_for_label_for_every_supported_encoding() {
611        for &enc in Encoding::ALL {
612            assert_eq!(
613                Encoding::for_label(enc.name()),
614                Some(enc),
615                "name = {}",
616                enc.name()
617            );
618            // Case-insensitivity holds for every encoding's canonical name too.
619            assert_eq!(
620                Encoding::for_label(&enc.name().to_lowercase()),
621                Some(enc),
622                "lowercased name = {}",
623                enc.name()
624            );
625            assert_eq!(
626                Encoding::for_label(&enc.name().to_uppercase()),
627                Some(enc),
628                "uppercased name = {}",
629                enc.name()
630            );
631        }
632    }
633
634    #[test]
635    fn all_is_free_of_duplicates() {
636        for (i, &a) in Encoding::ALL.iter().enumerate() {
637            for &b in &Encoding::ALL[i + 1..] {
638                assert_ne!(a, b, "Encoding::ALL contains a duplicate: {a}");
639            }
640        }
641    }
642
643    #[test]
644    fn display_matches_name() {
645        assert_eq!(Encoding::Utf8.to_string(), "UTF-8");
646        assert_eq!(Encoding::Windows1252.to_string(), "windows-1252");
647        assert_eq!(Encoding::ShiftJis.to_string(), "Shift_JIS");
648    }
649
650    #[test]
651    fn is_ascii_compatible_single_byte_matches_expectation() {
652        assert!(Encoding::Utf8.is_ascii_compatible_single_byte());
653        assert!(Encoding::Latin1.is_ascii_compatible_single_byte());
654        assert!(Encoding::Windows1252.is_ascii_compatible_single_byte());
655        assert!(Encoding::Ascii.is_ascii_compatible_single_byte());
656        assert!(Encoding::Iso8859_2.is_ascii_compatible_single_byte());
657        assert!(Encoding::Windows1251.is_ascii_compatible_single_byte());
658        assert!(Encoding::Koi8R.is_ascii_compatible_single_byte());
659        assert!(!Encoding::Utf16Le.is_ascii_compatible_single_byte());
660        assert!(!Encoding::Utf16Be.is_ascii_compatible_single_byte());
661        assert!(!Encoding::ShiftJis.is_ascii_compatible_single_byte());
662        assert!(!Encoding::EucJp.is_ascii_compatible_single_byte());
663        assert!(!Encoding::Iso2022Jp.is_ascii_compatible_single_byte());
664    }
665
666    #[test]
667    fn is_legacy_single_byte_table_matches_expectation() {
668        assert!(Encoding::Iso8859_2.is_legacy_single_byte_table());
669        assert!(Encoding::Windows1250.is_legacy_single_byte_table());
670        assert!(Encoding::Koi8R.is_legacy_single_byte_table());
671        assert!(Encoding::Macintosh.is_legacy_single_byte_table());
672        assert!(!Encoding::Latin1.is_legacy_single_byte_table());
673        assert!(!Encoding::Windows1252.is_legacy_single_byte_table());
674        assert!(!Encoding::Ascii.is_legacy_single_byte_table());
675        assert!(!Encoding::Utf8.is_legacy_single_byte_table());
676        assert!(!Encoding::ShiftJis.is_legacy_single_byte_table());
677        assert!(!Encoding::EucJp.is_legacy_single_byte_table());
678        assert!(!Encoding::Iso2022Jp.is_legacy_single_byte_table());
679    }
680
681    #[test]
682    fn all_contains_every_variant_exactly_once_via_new_decoder_smoke_test() {
683        // Not exhaustive by construction (nothing enforces that `ALL` is kept in sync with the
684        // enum at compile time), but constructing a decoder for every entry at least catches an
685        // entry that was added to `ALL` without the corresponding `Decoder::new` support (which
686        // would panic here rather than silently mis-decode).
687        for &encoding in Encoding::ALL {
688            let _ = encoding.new_decoder();
689        }
690    }
691}