Skip to main content

qubit_codec_text/charset/
charset.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8use core::{
9    fmt,
10    hash::{
11        Hash,
12        Hasher,
13    },
14};
15
16use crate::ByteOrder;
17
18/// Identifies the charset associated with a codec or error.
19///
20/// A charset is represented by a stable normalized identifier, a display
21/// name, and accepted aliases. Equality and hashing use only the identifier, so
22/// display names and alias lists can evolve without changing identity.
23///
24/// # Examples
25///
26/// ```rust
27/// use qubit_codec_text::Charset;
28///
29/// const GBK: Charset = Charset::new("gbk", "GBK", &["cp936"]);
30///
31/// assert!(GBK.matches_label("CP936"));
32/// assert_eq!(GBK, Charset::new("gbk", "Chinese GBK", &[]));
33/// assert_eq!("GBK", GBK.to_string());
34/// ```
35#[derive(Clone, Copy, Debug)]
36pub struct Charset {
37    /// Stable normalized identifier used for identity comparison.
38    id: &'static str,
39    /// Human-friendly display name for logs and errors.
40    name: &'static str,
41    /// Static alias list accepted in label matching.
42    aliases: &'static [&'static str],
43}
44
45impl Charset {
46    /// US-ASCII text.
47    pub const ASCII: Self = Self::new("ascii", "ASCII", &["us-ascii"]);
48
49    /// ISO-8859-1 / Latin-1 text.
50    pub const ISO_8859_1: Self = Self::new(
51        "iso-8859-1",
52        "ISO-8859-1",
53        &[
54            "latin1",
55            "latin-1",
56            "iso8859-1",
57            "csisolatin1",
58            "iso_8859-1",
59        ],
60    );
61
62    /// UTF-8 text.
63    pub const UTF_8: Self = Self::new("utf-8", "UTF-8", &["utf8"]);
64
65    /// UTF-16 text.
66    pub const UTF_16: Self = Self::new("utf-16", "UTF-16", &["utf16"]);
67
68    /// UTF-16 text serialized in little-endian byte order.
69    pub const UTF_16LE: Self = Self::new(
70        "utf-16le",
71        "UTF-16LE",
72        &["utf16le", "utf16_le", "utf_16_le"],
73    );
74
75    /// UTF-16 text serialized in big-endian byte order.
76    pub const UTF_16BE: Self = Self::new(
77        "utf-16be",
78        "UTF-16BE",
79        &["utf16be", "utf16_be", "utf_16_be"],
80    );
81
82    /// UTF-32 text.
83    pub const UTF_32: Self = Self::new("utf-32", "UTF-32", &["utf32"]);
84
85    /// UTF-32 text serialized in little-endian byte order.
86    pub const UTF_32LE: Self = Self::new(
87        "utf-32le",
88        "UTF-32LE",
89        &["utf32le", "utf32_le", "utf_32_le"],
90    );
91
92    /// UTF-32 text serialized in big-endian byte order.
93    pub const UTF_32BE: Self = Self::new(
94        "utf-32be",
95        "UTF-32BE",
96        &["utf32be", "utf32_be", "utf_32_be"],
97    );
98
99    /// Built-in charsets known by this crate.
100    pub const BUILTINS: &'static [Self] = &[
101        Self::ASCII,
102        Self::ISO_8859_1,
103        Self::UTF_8,
104        Self::UTF_16,
105        Self::UTF_16LE,
106        Self::UTF_16BE,
107        Self::UTF_32,
108        Self::UTF_32LE,
109        Self::UTF_32BE,
110    ];
111
112    /// Creates a charset descriptor.
113    ///
114    /// # Parameters
115    ///
116    /// - `id`: Stable normalized identifier used for equality and hashing.
117    /// - `name`: Human-readable display name.
118    /// - `aliases`: Additional labels accepted for this charset.
119    ///
120    /// # Returns
121    ///
122    /// Returns a charset descriptor carrying the supplied metadata.
123    #[inline(always)]
124    pub const fn new(
125        id: &'static str,
126        name: &'static str,
127        aliases: &'static [&'static str],
128    ) -> Self {
129        Self { id, name, aliases }
130    }
131
132    /// Finds a built-in charset by label.
133    ///
134    /// # Parameters
135    ///
136    /// - `label`: Charset label to match against built-in identifiers, names,
137    ///   and aliases.
138    ///
139    /// # Returns
140    ///
141    /// Returns `Some(Charset)` when `label` names one of this crate's built-in
142    /// charsets, or `None` when no built-in charset matches. Custom descriptors
143    /// created with [`Self::new`] are not part of this lookup. Leading and
144    /// trailing ASCII whitespace is ignored before matching.
145    pub fn from_label(label: &str) -> Option<Self> {
146        let label = label.trim_ascii();
147        Self::BUILTINS
148            .iter()
149            .copied()
150            .find(|charset| charset.matches_label(label))
151    }
152
153    /// Returns the stable normalized charset identifier.
154    ///
155    /// # Returns
156    ///
157    /// Returns the identifier used for equality and hashing.
158    #[inline(always)]
159    pub const fn id(self) -> &'static str {
160        self.id
161    }
162
163    /// Returns a human-readable charset label.
164    ///
165    /// # Returns
166    ///
167    /// Returns the display name stored in this descriptor.
168    #[inline(always)]
169    pub const fn name(self) -> &'static str {
170        self.name
171    }
172
173    /// Returns accepted aliases for this charset.
174    ///
175    /// # Returns
176    ///
177    /// Returns the static alias list stored in this descriptor.
178    #[inline(always)]
179    pub const fn aliases(self) -> &'static [&'static str] {
180        self.aliases
181    }
182
183    /// Returns the UTF-16 charset with a fixed byte order.
184    ///
185    /// # Parameters
186    ///
187    /// - `byte_order`: The byte order used by the byte stream.
188    ///
189    /// # Returns
190    ///
191    /// Returns [`Self::UTF_16LE`] for little-endian byte order and
192    /// [`Self::UTF_16BE`] for big-endian byte order.
193    #[inline(always)]
194    pub const fn from_utf16_byte_order(byte_order: ByteOrder) -> Self {
195        match byte_order {
196            ByteOrder::LittleEndian => Self::UTF_16LE,
197            ByteOrder::BigEndian => Self::UTF_16BE,
198        }
199    }
200
201    /// Returns the UTF-32 charset with a fixed byte order.
202    ///
203    /// # Parameters
204    ///
205    /// - `byte_order`: The byte order used by the byte stream.
206    ///
207    /// # Returns
208    ///
209    /// Returns [`Self::UTF_32LE`] for little-endian byte order and
210    /// [`Self::UTF_32BE`] for big-endian byte order.
211    #[inline(always)]
212    pub const fn from_utf32_byte_order(byte_order: ByteOrder) -> Self {
213        match byte_order {
214            ByteOrder::LittleEndian => Self::UTF_32LE,
215            ByteOrder::BigEndian => Self::UTF_32BE,
216        }
217    }
218
219    /// Returns the fixed byte order represented by this charset.
220    ///
221    /// # Returns
222    ///
223    /// Returns `Some(ByteOrder)` for fixed-endian UTF-16 and UTF-32 charsets.
224    /// Returns `None` for UTF-8 and generic UTF-16/UTF-32 charsets.
225    #[inline]
226    pub fn byte_order(self) -> Option<ByteOrder> {
227        if self == Self::UTF_16LE || self == Self::UTF_32LE {
228            Some(ByteOrder::LittleEndian)
229        } else if self == Self::UTF_16BE || self == Self::UTF_32BE {
230            Some(ByteOrder::BigEndian)
231        } else {
232            None
233        }
234    }
235
236    /// Tests whether a label names this charset.
237    ///
238    /// # Parameters
239    ///
240    /// - `label`: The label to compare with this descriptor's identifier,
241    ///   display name, and aliases.
242    ///
243    /// # Returns
244    ///
245    /// Returns `true` when `label` matches the identifier, display name, or one
246    /// of the aliases using ASCII case-insensitive comparison.
247    pub fn matches_label(self, label: &str) -> bool {
248        if label.eq_ignore_ascii_case(self.id)
249            || label.eq_ignore_ascii_case(self.name)
250        {
251            return true;
252        }
253        self.aliases
254            .iter()
255            .any(|alias| label.eq_ignore_ascii_case(alias))
256    }
257}
258
259impl PartialEq for Charset {
260    /// Compares charsets by stable identifier.
261    ///
262    /// # Parameters
263    ///
264    /// - `other`: The descriptor to compare against.
265    ///
266    /// # Returns
267    ///
268    /// Returns `true` when both descriptors have the same identifier.
269    #[inline(always)]
270    fn eq(&self, other: &Self) -> bool {
271        self.id == other.id
272    }
273}
274
275impl Eq for Charset {}
276
277impl Hash for Charset {
278    /// Hashes the stable encoding identifier.
279    ///
280    /// # Parameters
281    ///
282    /// - `state`: The hasher receiving this encoding's identity.
283    #[inline(always)]
284    fn hash<H>(&self, state: &mut H)
285    where
286        H: Hasher,
287    {
288        self.id.hash(state);
289    }
290}
291
292impl fmt::Display for Charset {
293    /// Formats this charset label.
294    ///
295    /// # Parameters
296    ///
297    /// - `formatter`: The formatter receiving the label.
298    ///
299    /// # Errors
300    ///
301    /// Returns any formatting error reported by `formatter`.
302    #[inline(always)]
303    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304        formatter.write_str(self.name())
305    }
306}