qubit_codec_text/codec/utf32_u32_codec.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 crate::{
9 Charset,
10 CharsetCodec,
11 CharsetDecodeError,
12 CharsetDecodeErrorKind,
13 CharsetDecodeResult,
14 CharsetEncodeError,
15 CharsetEncodeErrorKind,
16 CharsetEncodeProbe,
17 CharsetEncodeResult,
18 Unicode,
19 Utf32,
20};
21use qubit_codec::Codec;
22
23/// Combined UTF-32 `u32` code-unit codec.
24///
25/// `Utf32U32Codec` works with raw UTF-32 scalar-value units rather than
26/// serialized bytes. Use [`crate::Utf32ByteCodec`] for byte streams with an
27/// explicit byte order.
28///
29/// # Examples
30///
31/// ```rust
32/// use qubit_codec_text::{
33/// CharsetCodec,
34/// CharsetEncodeProbe,
35/// Codec,
36/// Charset,
37/// Utf32,
38/// Utf32U32Codec,
39/// };
40///
41/// let codec = Utf32U32Codec;
42/// assert_eq!(Charset::UTF_32, codec.charset());
43/// assert_eq!(Utf32::MAX_UNITS_PER_CHAR, codec.max_units_per_value().get());
44///
45/// let mut output = [0_u32; Utf32::MAX_UNITS_PER_CHAR];
46/// let written = codec.encode_len('中', 0).expect("mappable");
47/// unsafe {
48/// codec.encode_unchecked(&'中', &mut output, 0).expect("buffer fits");
49/// }
50/// let (value, consumed) = unsafe {
51/// codec.decode_unchecked(&output[..written], 0).expect("valid UTF-32")
52/// };
53/// assert_eq!(('中', written), (value, consumed.get()));
54/// ```
55#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
56pub struct Utf32U32Codec;
57
58impl Utf32U32Codec {
59 /// Returns the UTF-32 encoding descriptor.
60 ///
61 /// # Returns
62 ///
63 /// Returns [`Charset::UTF_32`].
64 #[must_use]
65 #[inline(always)]
66 pub const fn charset(self) -> Charset {
67 Charset::UTF_32
68 }
69}
70
71impl CharsetCodec for Utf32U32Codec {
72 /// Returns UTF-32 charset descriptor.
73 ///
74 /// # Returns
75 ///
76 /// Returns [`Charset::UTF_32`].
77 #[inline(always)]
78 fn charset(&self) -> Charset {
79 Charset::UTF_32
80 }
81}
82
83impl CharsetEncodeProbe for Utf32U32Codec {
84 /// Encodes one Unicode scalar value into a `u32` unit at `index`.
85 ///
86 /// # Arguments
87 ///
88 /// * `ch` - The Unicode scalar value to encode.
89 /// * `index` - Input character index used for error context.
90 ///
91 /// # Returns
92 ///
93 /// Always returns `Ok(1)`.
94 #[inline(always)]
95 fn encode_len(
96 &self,
97 _ch: char,
98 _index: usize,
99 ) -> CharsetEncodeResult<usize> {
100 Ok(Utf32::MAX_UNITS_PER_CHAR)
101 }
102}
103
104unsafe impl Codec for Utf32U32Codec {
105 type Value = char;
106 type Unit = u32;
107 type DecodeError = CharsetDecodeError;
108 type EncodeError = CharsetEncodeError;
109
110 #[inline(always)]
111 fn min_units_per_value(&self) -> core::num::NonZeroUsize {
112 core::num::NonZeroUsize::MIN
113 }
114
115 #[inline(always)]
116 fn max_units_per_value(&self) -> core::num::NonZeroUsize {
117 core::num::NonZeroUsize::MIN
118 }
119
120 #[inline(always)]
121 unsafe fn decode_unchecked(
122 &self,
123 input: &[u32],
124 index: usize,
125 ) -> CharsetDecodeResult<(char, core::num::NonZeroUsize)> {
126 let (ch, consumed) = decode_units_prefix(input, index)?;
127 debug_assert!(consumed.get() <= input.len() - index);
128 Ok((ch, consumed))
129 }
130
131 #[inline(always)]
132 unsafe fn encode_unchecked(
133 &self,
134 ch: &char,
135 output: &mut [u32],
136 index: usize,
137 ) -> CharsetEncodeResult<usize> {
138 let written = encode_units_char(*ch, output, index)?;
139 debug_assert_eq!(written, Utf32::MAX_UNITS_PER_CHAR);
140 debug_assert!(written <= output.len() - index);
141 Ok(written)
142 }
143}
144
145/// Decodes the first UTF-32 character from a closed `u32` buffer.
146///
147/// Each UTF-32 unit is interpreted as a Unicode scalar value directly.
148///
149/// # Arguments
150///
151/// * `input` - UTF-32 unit slice to decode from.
152/// * `index` - Start offset in `input`; must be `<= input.len()`.
153///
154/// # Returns
155///
156/// Returns the decoded character and `1` consumed unit.
157///
158/// # Errors
159///
160/// * `CharsetDecodeErrorKind::InvalidInputIndex` when `index` is greater than
161/// `input.len()`.
162/// * `CharsetDecodeErrorKind::IncompleteSequence` when EOF appears before one
163/// UTF-32 unit is available.
164/// * `CharsetDecodeErrorKind::InvalidCodePoint` when `input[index]` is not a
165/// valid scalar.
166#[inline]
167fn decode_units_prefix(
168 input: &[u32],
169 index: usize,
170) -> CharsetDecodeResult<(char, core::num::NonZeroUsize)> {
171 if index > input.len() {
172 let kind = CharsetDecodeErrorKind::InvalidInputIndex {
173 input_len: input.len(),
174 };
175 return Err(CharsetDecodeError::new(Charset::UTF_32, kind, index));
176 }
177 if index == input.len() {
178 let kind = CharsetDecodeErrorKind::IncompleteSequence {
179 required: 1,
180 available: 0,
181 };
182 return Err(CharsetDecodeError::new(Charset::UTF_32, kind, index));
183 }
184 match Unicode::to_char(input[index]) {
185 Some(ch) => Ok((ch, core::num::NonZeroUsize::MIN)),
186 None => {
187 let kind = CharsetDecodeErrorKind::InvalidCodePoint {
188 value: input[index],
189 };
190 Err(CharsetDecodeError::new(Charset::UTF_32, kind, index))
191 }
192 }
193}
194
195/// Encodes one character into a UTF-32 `u32` unit at `index` in `output`.
196///
197/// # Arguments
198///
199/// * `ch` - The character to encode.
200/// * `output` - Destination unit buffer.
201/// * `index` - Start offset in `output`; must satisfy `index < output.len()`.
202///
203/// # Returns
204///
205/// Always `Ok(1)` to indicate one unit was written.
206///
207/// # Errors
208///
209/// * `CharsetEncodeErrorKind::BufferTooSmall` when no unit can be written at
210/// `index`.
211#[inline]
212fn encode_units_char(
213 ch: char,
214 output: &mut [u32],
215 index: usize,
216) -> CharsetEncodeResult<usize> {
217 if index >= output.len() {
218 let kind = CharsetEncodeErrorKind::BufferTooSmall {
219 required: required_index(index, 1),
220 available: 0,
221 };
222 return Err(CharsetEncodeError::new(Charset::UTF_32, kind, index));
223 }
224 output[index] = ch as u32;
225 Ok(1)
226}
227
228#[inline(always)]
229const fn required_index(index: usize, required_units: usize) -> usize {
230 match index.checked_add(required_units) {
231 Some(required) => required,
232 None => usize::MAX,
233 }
234}