Skip to main content

qubit_text_codec/codec/
charset_encoder.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use core::fmt;
11
12use crate::{
13    CharsetEncodeError,
14    CharsetEncodeErrorKind,
15    CharsetEncodeResult,
16    Coder,
17    CoderProgress,
18    CoderStatus,
19};
20
21use super::{
22    charset_codec::CharsetCodec,
23    unmappable_action::UnmappableAction,
24};
25
26/// Converts Unicode scalar values into units of one charset.
27///
28/// `CharsetEncoder` wraps a low-level [`CharsetCodec`] and applies the
29/// configured [`UnmappableAction`] whenever the codec reports that an input
30/// character cannot be represented by the target charset.
31///
32/// # Type Parameters
33///
34/// - `C`: Low-level charset codec used to encode one character into target
35///   storage units.
36#[derive(Clone)]
37pub struct CharsetEncoder<C>
38where
39    C: CharsetCodec,
40{
41    /// Low-level codec used for target encoding.
42    codec: C,
43    /// Action used for unmappable input characters.
44    unmappable_action: UnmappableAction,
45    /// Replacement character used by [`UnmappableAction::Replace`].
46    replacement: char,
47    /// Pre-encoded units for the configured replacement character.
48    replacement_units: Vec<C::Unit>,
49}
50
51impl<C> CharsetEncoder<C>
52where
53    C: CharsetCodec,
54{
55    /// Default replacement character used when unmappable input is replaced.
56    pub const DEFAULT_REPLACEMENT: char = '\u{fffd}';
57
58    /// Fallback replacement used when the default replacement is unmappable.
59    pub const DEFAULT_FALLBACK_REPLACEMENT: char = '?';
60
61    /// Creates an encoder with default replacement policy.
62    ///
63    /// # Parameters
64    ///
65    /// - `codec`: Low-level charset codec used to encode output units.
66    ///
67    /// # Returns
68    ///
69    /// Returns an encoder whose unmappable action is
70    /// [`UnmappableAction::Replace`] and whose replacement character is
71    /// [`CharsetEncoder::DEFAULT_REPLACEMENT`]. If the default cannot be encoded
72    /// by the codec, [`CharsetEncoder::DEFAULT_FALLBACK_REPLACEMENT`] is used.
73    #[must_use]
74    pub fn new(codec: C) -> Self {
75        let mut encoder = Self {
76            codec,
77            unmappable_action: UnmappableAction::Replace,
78            replacement: Self::DEFAULT_REPLACEMENT,
79            replacement_units: Vec::new(),
80        };
81        match encoder.encode_replacement(Self::DEFAULT_REPLACEMENT) {
82            Ok(replacement_units) => {
83                encoder.replacement = Self::DEFAULT_REPLACEMENT;
84                encoder.replacement_units = replacement_units;
85                encoder
86            }
87            Err(default_error) => match encoder.encode_replacement(Self::DEFAULT_FALLBACK_REPLACEMENT) {
88                Ok(replacement_units) => {
89                    encoder.replacement = Self::DEFAULT_FALLBACK_REPLACEMENT;
90                    encoder.replacement_units = replacement_units;
91                    encoder
92                }
93                Err(_) => panic!(
94                    "cannot initialize CharsetEncoder for {:?}: neither {:?} nor {:?} is encodable ({default_error})",
95                    encoder.codec.charset(),
96                    Self::DEFAULT_REPLACEMENT,
97                    Self::DEFAULT_FALLBACK_REPLACEMENT,
98                ),
99            },
100        }
101    }
102
103    /// Creates an encoder with the provided replacement character.
104    ///
105    /// The replacement character is checked once on construction. If the codec
106    /// cannot encode it, this returns an error immediately.
107    ///
108    /// # Parameters
109    ///
110    /// - `replacement`: Replacement character for unmappable input.
111    ///
112    /// # Returns
113    ///
114    /// - `Ok(Self)` when the character is encodable by the codec.
115    /// - `Err(Self::Error)` when the replacement is unsupported.
116    #[inline]
117    pub fn with_replacement(mut self, replacement: char) -> Result<Self, CharsetEncodeError> {
118        let replacement_units = self.encode_replacement(replacement)?;
119        self.replacement = replacement;
120        self.replacement_units = replacement_units;
121        Ok(self)
122    }
123
124    /// Returns the wrapped low-level codec.
125    ///
126    /// # Returns
127    ///
128    /// Returns a shared reference to the configured codec.
129    #[must_use]
130    #[inline]
131    pub const fn codec(&self) -> &C {
132        &self.codec
133    }
134
135    /// Returns a mutable reference to the wrapped codec.
136    ///
137    /// # Returns
138    ///
139    /// Returns a mutable reference to the configured codec.
140    #[must_use]
141    #[inline]
142    pub fn codec_mut(&mut self) -> &mut C {
143        &mut self.codec
144    }
145
146    /// Returns the configured unmappable-character action.
147    ///
148    /// # Returns
149    ///
150    /// Returns the action used when target encoding cannot represent a character.
151    #[must_use]
152    #[inline]
153    pub const fn unmappable_action(&self) -> UnmappableAction {
154        self.unmappable_action
155    }
156
157    /// Sets the unmappable-character action.
158    ///
159    /// # Parameters
160    ///
161    /// - `action`: New policy for unmappable input characters.
162    #[inline]
163    pub fn set_unmappable_action(&mut self, action: UnmappableAction) {
164        self.unmappable_action = action;
165    }
166
167    /// Returns the configured replacement character.
168    ///
169    /// # Returns
170    ///
171    /// Returns the character encoded when [`UnmappableAction::Replace`] is used.
172    #[must_use]
173    #[inline]
174    pub const fn replacement(&self) -> char {
175        self.replacement
176    }
177
178    /// Sets the replacement character.
179    ///
180    /// # Parameters
181    ///
182    /// - `replacement`: New replacement character used by replace policy.
183    ///
184    /// # Errors
185    ///
186    /// Returns `Err` when the codec cannot encode the given replacement.
187    #[inline]
188    pub fn set_replacement(&mut self, replacement: char) -> Result<(), CharsetEncodeError> {
189        let replacement_units = self.encode_replacement(replacement)?;
190        self.replacement = replacement;
191        self.replacement_units = replacement_units;
192        Ok(())
193    }
194
195    /// Encodes a replacement character into a temporary buffer and returns the
196    /// encoded unit sequence.
197    ///
198    /// # Parameters
199    ///
200    /// - `ch`: Replacement character to validate and encode.
201    ///
202    /// # Returns
203    ///
204    /// - `Ok(Vec<C::Unit>)` when the character is encodable.
205    /// - `Err(CharsetEncodeError)` with codec-specific context when encoding fails.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error when the target charset cannot encode the character.
210    #[inline]
211    fn encode_replacement(&self, ch: char) -> CharsetEncodeResult<Vec<C::Unit>> {
212        let mut output = vec![C::Unit::default(); self.codec.max_units_per_char().max(1)];
213        let written = self.encode_char_to_units(ch, output.as_mut_slice(), 0, |_, _, error| Err(error))?;
214        output.truncate(written);
215        Ok(output)
216    }
217
218    /// Encodes a single Unicode scalar value into the caller-provided unit buffer.
219    ///
220    /// This is the common template around [`CharsetCodec::encode_one`]. It
221    /// keeps all direct codec error inspection in one place while allowing the
222    /// caller to decide how an unmappable character should be handled.
223    ///
224    /// # Parameters
225    ///
226    /// - `ch`: Character to encode.
227    /// - `output`: Target unit buffer to write into.
228    /// - `output_index`: Start position in `output` to write the encoded units.
229    /// - `on_unmappable`: Handler called when the codec reports
230    ///   [`CharsetEncodeErrorKind::UnmappableCharacter`].
231    ///
232    /// # Returns
233    ///
234    /// - `Ok(usize)` of how many units were written.
235    /// - `Err(CharsetEncodeError)` when encoding fails.
236    ///
237    /// # Errors
238    ///
239    /// - `CharsetEncodeError` if the codec cannot encode the character.
240    fn encode_char_to_units(
241        &self,
242        ch: char,
243        output: &mut [C::Unit],
244        output_index: usize,
245        on_unmappable: impl FnOnce(&mut [C::Unit], usize, CharsetEncodeError) -> CharsetEncodeResult<usize>,
246    ) -> CharsetEncodeResult<usize> {
247        match self.codec.encode_one(ch, output, output_index) {
248            Ok(written) => Ok(written),
249            Err(error) => match error.kind() {
250                CharsetEncodeErrorKind::UnmappableCharacter { .. } => on_unmappable(output, output_index, error),
251                CharsetEncodeErrorKind::BufferTooSmall { .. }
252                | CharsetEncodeErrorKind::InvalidInputIndex { .. }
253                | CharsetEncodeErrorKind::InvalidCodePoint { .. } => Err(error),
254            },
255        }
256    }
257
258    /// Writes the cached replacement units into the target output slice.
259    ///
260    /// # Parameters
261    ///
262    /// - `output`: Complete target output slice.
263    /// - `output_index`: Absolute output index where replacement writing starts.
264    ///
265    /// # Returns
266    ///
267    /// Returns the number of output units written for the replacement.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`CharsetEncodeError`] when the output buffer is too small.
272    #[inline]
273    fn write_replacement(&self, output: &mut [C::Unit], output_index: usize) -> CharsetEncodeResult<usize> {
274        let available = output.len().saturating_sub(output_index);
275        if available < self.replacement_units.len() {
276            let kind = CharsetEncodeErrorKind::BufferTooSmall {
277                required: output_index + self.replacement_units.len(),
278                available,
279            };
280            return Err(CharsetEncodeError::new(self.codec.charset(), kind, output_index));
281        }
282        if self.replacement_units.is_empty() {
283            return Ok(0);
284        }
285        let end = output_index + self.replacement_units.len();
286        output[output_index..end].copy_from_slice(&self.replacement_units[..]);
287        Ok(self.replacement_units.len())
288    }
289}
290
291impl<C> Coder<char, C::Unit> for CharsetEncoder<C>
292where
293    C: CharsetCodec,
294{
295    type Error = CharsetEncodeError;
296
297    /// Returns the maximum number of target units needed for `input_len` characters.
298    #[inline]
299    fn max_output_len(&self, input_len: usize) -> Option<usize> {
300        input_len.checked_mul(self.codec.max_units_per_char())
301    }
302
303    /// Encodes characters into the target charset while applying unmappable policy.
304    fn convert(
305        &mut self,
306        input: &[char],
307        input_index: usize,
308        output: &mut [C::Unit],
309        output_index: usize,
310    ) -> Result<CoderProgress, Self::Error> {
311        if input_index > input.len() {
312            let kind = CharsetEncodeErrorKind::InvalidInputIndex { input_len: input.len() };
313            return Err(CharsetEncodeError::new(self.codec.charset(), kind, input_index));
314        }
315        if output_index > output.len() {
316            let status = CoderStatus::NeedOutput {
317                output_index,
318                required: 1,
319                available: 0,
320            };
321            return Ok(CoderProgress::new(status, 0, 0));
322        }
323
324        let mut input_cursor = input_index;
325        let mut output_cursor = output_index;
326        while input_cursor < input.len() {
327            let ch = input[input_cursor];
328            match self.encode_char_to_units(ch, output, output_cursor, |output, output_index, _| {
329                match self.unmappable_action {
330                    UnmappableAction::Report => {
331                        let kind = CharsetEncodeErrorKind::UnmappableCharacter { value: ch as u32 };
332                        Err(CharsetEncodeError::new(self.codec.charset(), kind, input_cursor))
333                    }
334                    UnmappableAction::Ignore => Ok(0),
335                    UnmappableAction::Replace => self.write_replacement(output, output_index),
336                }
337            }) {
338                Ok(written) => {
339                    input_cursor += 1;
340                    output_cursor += written;
341                }
342                Err(error) if matches!(error.kind(), CharsetEncodeErrorKind::BufferTooSmall { .. }) => {
343                    let required = error
344                        .required()
345                        .unwrap_or(output_cursor + 1)
346                        .saturating_sub(output_cursor);
347                    let available = error.available().unwrap_or(0);
348                    let status = CoderStatus::NeedOutput {
349                        output_index: output_cursor,
350                        required,
351                        available,
352                    };
353                    return Ok(CoderProgress::new(
354                        status,
355                        input_cursor - input_index,
356                        output_cursor - output_index,
357                    ));
358                }
359                Err(error) => {
360                    return Err(error);
361                }
362            }
363        }
364        Ok(CoderProgress::complete(
365            input_cursor - input_index,
366            output_cursor - output_index,
367        ))
368    }
369}
370
371impl<C> Eq for CharsetEncoder<C> where C: CharsetCodec + Eq {}
372
373impl<C> PartialEq for CharsetEncoder<C>
374where
375    C: CharsetCodec + PartialEq,
376{
377    /// Compares encoder configuration without leaking cached-unit trait bounds.
378    fn eq(&self, other: &Self) -> bool {
379        self.codec == other.codec
380            && self.unmappable_action == other.unmappable_action
381            && self.replacement == other.replacement
382    }
383}
384
385impl<C> fmt::Debug for CharsetEncoder<C>
386where
387    C: CharsetCodec + fmt::Debug,
388{
389    /// Formats the encoder without exposing additional bounds for cached units.
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        f.debug_struct("CharsetEncoder")
392            .field("codec", &self.codec)
393            .field("unmappable_action", &self.unmappable_action)
394            .field("replacement", &self.replacement)
395            .field("replacement_units_len", &self.replacement_units.len())
396            .finish()
397    }
398}