Skip to main content

qubit_codec_text/codec/
utf16_u16_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    Utf16,
20};
21use qubit_codec::Codec;
22
23/// Combined UTF-16 `u16` code-unit codec.
24///
25/// `Utf16U16Codec` works with UTF-16 code units rather than serialized bytes.
26/// Use [`crate::Utf16ByteCodec`] when the input or output is a byte stream with
27/// an explicit byte order.
28///
29/// # Examples
30///
31/// ```rust
32/// use qubit_codec_text::{
33///     CharsetCodec,
34///     CharsetEncodeProbe,
35///     Codec,
36///     Charset,
37///     Utf16,
38///     Utf16U16Codec,
39/// };
40///
41/// let codec = Utf16U16Codec;
42/// assert_eq!(Charset::UTF_16, codec.charset());
43/// assert_eq!(Utf16::MAX_UNITS_PER_CHAR, codec.max_units_per_value().get());
44///
45/// let mut output = [0_u16; Utf16::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-16")
52/// };
53/// assert_eq!(('😀', written), (value, consumed.get()));
54/// ```
55#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
56pub struct Utf16U16Codec;
57
58impl Utf16U16Codec {
59    /// Returns the UTF-16 encoding descriptor.
60    ///
61    /// # Returns
62    ///
63    /// Returns [`Charset::UTF_16`].
64    #[must_use]
65    #[inline(always)]
66    pub const fn charset(self) -> Charset {
67        Charset::UTF_16
68    }
69}
70
71impl CharsetCodec for Utf16U16Codec {
72    /// Returns UTF-16 charset descriptor.
73    ///
74    /// # Returns
75    ///
76    /// Returns [`Charset::UTF_16`].
77    #[inline(always)]
78    fn charset(&self) -> Charset {
79        Charset::UTF_16
80    }
81}
82
83impl CharsetEncodeProbe for Utf16U16Codec {
84    /// Encodes one Unicode scalar value into UTF-16 code units 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    /// `Ok(usize)` with the required UTF-16 units (`1` or `2`).
94    #[inline(always)]
95    fn encode_len(
96        &self,
97        ch: char,
98        _index: usize,
99    ) -> CharsetEncodeResult<usize> {
100        Ok(Utf16::unit_len(ch))
101    }
102}
103
104unsafe impl Codec for Utf16U16Codec {
105    type Value = char;
106    type Unit = u16;
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        // SAFETY: UTF-16 encodes every scalar value as at least one unit.
118        unsafe {
119            core::num::NonZeroUsize::new_unchecked(Utf16::MAX_UNITS_PER_CHAR)
120        }
121    }
122
123    #[inline(always)]
124    unsafe fn decode_unchecked(
125        &self,
126        input: &[u16],
127        index: usize,
128    ) -> CharsetDecodeResult<(char, core::num::NonZeroUsize)> {
129        let (ch, consumed) = decode_units_prefix(input, index)?;
130        debug_assert!(consumed.get() <= input.len() - index);
131        Ok((ch, consumed))
132    }
133
134    #[inline(always)]
135    unsafe fn encode_unchecked(
136        &self,
137        ch: &char,
138        output: &mut [u16],
139        index: usize,
140    ) -> CharsetEncodeResult<usize> {
141        let written = encode_units_char(*ch, output, index)?;
142        debug_assert_eq!(written, ch.len_utf16());
143        debug_assert!(written <= output.len() - index);
144        Ok(written)
145    }
146}
147
148/// Decodes the first UTF-16 character from a closed `u16` buffer.
149///
150/// The function handles three cases:
151/// 1. ASCII/non-surrogate units decode to a single `char`.
152/// 2. High-surrogate pairs are combined with the following unit into one scalar
153///    value.
154/// 3. Isolated low-surrogates are rejected as malformed.
155///
156/// # Arguments
157///
158/// * `input` - UTF-16 unit slice to decode from. Normal streaming callers
159///   provide at least [`Utf16::MAX_UNITS_PER_CHAR`] readable units unless EOF
160///   has been reached.
161/// * `index` - Start offset in `input`; must be `<= input.len()`.
162///
163/// # Returns
164///
165/// Returns the decoded character and the non-zero number of consumed UTF-16
166/// units.
167///
168/// # Errors
169///
170/// * `CharsetDecodeErrorKind::InvalidInputIndex` when `index` is greater than
171///   `input.len()`.
172/// * `CharsetDecodeErrorKind::MalformedSequence` for invalid UTF-16 sequences
173///   (invalid high/low surrogate pairing).
174/// * `CharsetDecodeErrorKind::IncompleteSequence` when EOF appears before a
175///   complete surrogate pair is available.
176///
177/// # Panics
178///
179/// This function does not panic for invalid UTF-16 input because invalid input
180/// is surfaced as `CharsetDecodeError`.
181#[inline]
182fn decode_units_prefix(
183    input: &[u16],
184    index: usize,
185) -> CharsetDecodeResult<(char, core::num::NonZeroUsize)> {
186    if index > input.len() {
187        let kind = CharsetDecodeErrorKind::InvalidInputIndex {
188            input_len: input.len(),
189        };
190        return Err(CharsetDecodeError::new(Charset::UTF_16, kind, index));
191    }
192    if index == input.len() {
193        let kind = CharsetDecodeErrorKind::IncompleteSequence {
194            required: 1,
195            available: 0,
196        };
197        return Err(CharsetDecodeError::new(Charset::UTF_16, kind, index));
198    }
199    let first = input[index];
200    if Utf16::is_high_surrogate(first) {
201        if !has_units(input.len(), index, 2) {
202            let kind = CharsetDecodeErrorKind::IncompleteSequence {
203                required: 2,
204                available: input.len() - index,
205            };
206            return Err(CharsetDecodeError::new(Charset::UTF_16, kind, index));
207        }
208        let second = input[index + 1];
209        match Utf16::compose_pair(first, second).and_then(Unicode::to_char) {
210            Some(ch) => {
211                // SAFETY: 2 is non-zero.
212                Ok((ch, unsafe { core::num::NonZeroUsize::new_unchecked(2) }))
213            }
214            None => {
215                let kind = CharsetDecodeErrorKind::MalformedSequence {
216                    value: Some(second as u32),
217                };
218                Err(CharsetDecodeError::new(
219                    Charset::UTF_16,
220                    kind,
221                    required_index(index, 1),
222                )
223                .with_consumed(2))
224            }
225        }
226    } else if Utf16::is_low_surrogate(first) {
227        let kind = CharsetDecodeErrorKind::MalformedSequence {
228            value: Some(first as u32),
229        };
230        Err(CharsetDecodeError::new(Charset::UTF_16, kind, index))
231    } else {
232        let ch = char::from_u32(first as u32)
233            .expect("non-surrogate UTF-16 unit is a scalar value");
234        Ok((ch, core::num::NonZeroUsize::MIN))
235    }
236}
237
238/// Encodes one character into UTF-16 `u16` units at `index` in `output`.
239///
240/// The helper returns how many UTF-16 units are written:
241/// one for BMP scalars, two for supplementary scalars.
242///
243/// # Arguments
244///
245/// * `ch` - The character to encode.
246/// * `output` - Destination unit buffer.
247/// * `index` - Start offset in `output`; must be `<= output.len()`.
248///
249/// # Returns
250///
251/// `Ok(usize)` with the number of written UTF-16 units (`1` or `2`).
252///
253/// # Errors
254///
255/// * `CharsetEncodeErrorKind::BufferTooSmall` when insufficient room exists
256///   from `index`.
257#[inline]
258fn encode_units_char(
259    ch: char,
260    output: &mut [u16],
261    index: usize,
262) -> CharsetEncodeResult<usize> {
263    if index > output.len() {
264        let kind = CharsetEncodeErrorKind::BufferTooSmall {
265            required: required_index(index, 1),
266            available: 0,
267        };
268        return Err(CharsetEncodeError::new(Charset::UTF_16, kind, index));
269    }
270    let length = Utf16::unit_len(ch);
271    let available = output.len() - index;
272    if available < length {
273        let kind = CharsetEncodeErrorKind::BufferTooSmall {
274            required: required_index(index, length),
275            available,
276        };
277        return Err(CharsetEncodeError::new(Charset::UTF_16, kind, index));
278    }
279    let code_point = ch as u32;
280    if length == 1 {
281        output[index] = code_point as u16;
282    } else {
283        output[index] = Utf16::high_surrogate(code_point)
284            .expect("supplementary scalar has high surrogate");
285        output[index + 1] = Utf16::low_surrogate(code_point)
286            .expect("supplementary scalar has low surrogate");
287    }
288    Ok(length)
289}
290
291#[inline(always)]
292const fn has_units(len: usize, index: usize, required_units: usize) -> bool {
293    match index.checked_add(required_units) {
294        Some(end) => len >= end,
295        None => false,
296    }
297}
298
299#[inline(always)]
300const fn required_index(index: usize, required_units: usize) -> usize {
301    match index.checked_add(required_units) {
302        Some(required) => required,
303        None => usize::MAX,
304    }
305}