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}