Skip to main content

qubit_codec_text/codec/
charset_converter.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 super::{
9    charset_codec::CharsetCodec,
10    charset_convert_error::CharsetConvertError,
11    charset_convert_hooks::CharsetConvertHooks,
12};
13use crate::{
14    CharsetDecodePolicy,
15    CharsetEncodeError,
16    CharsetEncodeErrorKind,
17    CharsetEncodeHooks,
18    CharsetEncodePolicy,
19    CharsetEncodeProbe,
20    CharsetEncoder,
21    MalformedAction,
22    UnmappableAction,
23};
24use qubit_codec::{
25    BufferedConvertEngine,
26    BufferedConverter,
27    BufferedTranscoder,
28    CapacityError,
29    FinishError,
30    TranscodeProgress,
31};
32
33/// Converts units encoded with one charset into units encoded with another
34/// charset.
35///
36/// The converter owns the source and target charset codecs plus the same
37/// decode/encode policy hooks used by [`crate::CharsetDecoder`] and
38/// [`crate::CharsetEncoder`].
39/// A decoded character may be kept pending inside the common buffered convert
40/// engine when the target output buffer is full. During
41/// [`BufferedTranscoder::finish`], the converter drains internally retained
42/// output and finishes the composed decode/encode policy hooks. Callers remain
43/// responsible for handling any incomplete input tail before finishing the
44/// logical stream.
45///
46/// # Type Parameters
47///
48/// - `D`: Low-level charset codec used by the source decoder.
49/// - `E`: Low-level charset codec used by the target encoder.
50///
51/// ```rust
52/// use qubit_codec_text::{
53///     CharsetConverter,
54///     CharsetDecoder,
55///     CharsetEncoder,
56///     TranscodeStatus,
57///     BufferedTranscoder,
58///     Utf16U16Codec,
59///     Utf8Codec,
60/// };
61///
62/// let mut converter = CharsetConverter::from_codecs(Utf8Codec, Utf16U16Codec);
63/// let mut output = [0_u16; 2];
64///
65/// let progress = converter
66///     .transcode("AB".as_bytes(), 0, &mut output, 0)
67///     .expect("transcode bytes to utf-16");
68///
69/// assert_eq!(TranscodeStatus::Complete, progress.status());
70/// assert_eq!(2, progress.read());
71/// assert_eq!(2, progress.written());
72/// assert_eq!([65, 66], output);
73/// ```
74#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct CharsetConverter<D, E>
76where
77    D: CharsetCodec,
78    E: CharsetEncodeProbe,
79{
80    /// Common buffered converter engine.
81    engine: BufferedConvertEngine<D, E, CharsetConvertHooks<E::Unit>>,
82    /// Public malformed-input policy metadata.
83    decode_policy: CharsetDecodePolicy,
84    /// Public unmappable-input policy metadata.
85    encode_policy: CharsetEncodePolicy,
86}
87
88impl<D, E> CharsetConverter<D, E>
89where
90    D: CharsetCodec,
91    E: CharsetEncodeProbe,
92{
93    /// Creates a charset converter from raw source and target codecs.
94    ///
95    /// # Parameters
96    ///
97    /// - `source`: Source charset codec.
98    /// - `target`: Target charset codec.
99    ///
100    /// # Returns
101    ///
102    /// Returns a converter with the default decoder policy and the target
103    /// encoder policy that can be represented by `target`. The encoder policy
104    /// first tries [`CharsetEncodePolicy::DEFAULT_REPLACEMENT`] and falls back
105    /// to [`CharsetEncodePolicy::DEFAULT_FALLBACK_REPLACEMENT`] when needed.
106    ///
107    /// # Panics
108    ///
109    /// Panics when neither default replacement can be encoded by `target`.
110    #[must_use]
111    pub fn from_codecs(source: D, target: E) -> Self {
112        let decode_policy = CharsetDecodePolicy::default();
113        let (encode_policy, encode_hooks) =
114            Self::default_encode_policy(&target);
115        Self {
116            engine: BufferedConvertEngine::new(
117                source,
118                target,
119                CharsetConvertHooks::with_policies(
120                    decode_policy,
121                    encode_policy,
122                    encode_hooks,
123                ),
124            ),
125            decode_policy,
126            encode_policy,
127        }
128    }
129
130    /// Creates a charset converter from raw codecs and explicit policies.
131    ///
132    /// # Parameters
133    ///
134    /// - `source`: Source charset codec.
135    /// - `target`: Target charset codec.
136    /// - `decode_policy`: Malformed source-input policy.
137    /// - `encode_policy`: Unmappable target-output policy.
138    ///
139    /// # Returns
140    ///
141    /// Returns a converter configured with the supplied policies.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error when `encode_policy` uses replacement and the target
146    /// codec cannot encode the replacement character.
147    pub fn from_codecs_with_policies(
148        source: D,
149        target: E,
150        decode_policy: CharsetDecodePolicy,
151        encode_policy: CharsetEncodePolicy,
152    ) -> Result<Self, CharsetEncodeError> {
153        let (encode_hooks, _) =
154            CharsetEncoder::<E>::create_hooks(&target, encode_policy)?;
155        Ok(Self {
156            engine: BufferedConvertEngine::new(
157                source,
158                target,
159                CharsetConvertHooks::with_policies(
160                    decode_policy,
161                    encode_policy,
162                    encode_hooks,
163                ),
164            ),
165            decode_policy,
166            encode_policy,
167        })
168    }
169
170    /// Returns the configured malformed source-input policy.
171    ///
172    /// # Returns
173    ///
174    /// Returns the decoder policy used by this converter.
175    #[must_use]
176    #[inline(always)]
177    pub const fn decode_policy(&self) -> CharsetDecodePolicy {
178        self.decode_policy
179    }
180
181    /// Returns the configured unmappable target-output policy.
182    ///
183    /// # Returns
184    ///
185    /// Returns the encoder policy used by this converter.
186    #[must_use]
187    #[inline(always)]
188    pub const fn encode_policy(&self) -> CharsetEncodePolicy {
189        self.encode_policy
190    }
191
192    /// Returns the configured malformed-input action.
193    ///
194    /// # Returns
195    ///
196    /// Returns the action used when source input is malformed.
197    #[must_use]
198    #[inline(always)]
199    pub const fn malformed_action(&self) -> MalformedAction {
200        self.decode_policy.malformed_action()
201    }
202
203    /// Returns the configured source replacement character.
204    ///
205    /// # Returns
206    ///
207    /// Returns the character emitted when malformed source input is replaced.
208    #[must_use]
209    #[inline(always)]
210    pub const fn decode_replacement(&self) -> char {
211        self.decode_policy.replacement()
212    }
213
214    /// Returns the configured unmappable-character action.
215    ///
216    /// # Returns
217    ///
218    /// Returns the action used when the target charset cannot represent a
219    /// character.
220    #[must_use]
221    #[inline(always)]
222    pub const fn unmappable_action(&self) -> UnmappableAction {
223        self.encode_policy.unmappable_action()
224    }
225
226    /// Returns the configured target replacement character.
227    ///
228    /// # Returns
229    ///
230    /// Returns the character encoded when unmappable target input is replaced.
231    #[must_use]
232    #[inline(always)]
233    pub const fn replacement(&self) -> char {
234        self.encode_policy.replacement()
235    }
236
237    /// Returns the default encode policy that can be represented by `target`.
238    ///
239    /// # Panics
240    ///
241    /// Panics when neither the default replacement nor the fallback replacement
242    /// can be encoded by `target`.
243    fn default_encode_policy(
244        target: &E,
245    ) -> (CharsetEncodePolicy, CharsetEncodeHooks<E::Unit>) {
246        let default_policy = CharsetEncodePolicy::default();
247        match CharsetEncoder::<E>::create_hooks(target, default_policy) {
248            Ok((hooks, _)) => (default_policy, hooks),
249            Err(_) => {
250                let fallback_policy = CharsetEncodePolicy::replace(
251                    CharsetEncodePolicy::DEFAULT_FALLBACK_REPLACEMENT,
252                );
253                if let Ok((hooks, _)) =
254                    CharsetEncoder::<E>::create_hooks(target, fallback_policy)
255                {
256                    return (fallback_policy, hooks);
257                }
258                let kind = CharsetEncodeErrorKind::UnmappableCharacter {
259                    value: CharsetEncodePolicy::DEFAULT_FALLBACK_REPLACEMENT
260                        as u32,
261                };
262                panic!(
263                    "cannot initialize CharsetConverter target for {:?}: neither {:?} nor {:?} is encodable ({})",
264                    target.charset(),
265                    CharsetEncodePolicy::DEFAULT_REPLACEMENT,
266                    CharsetEncodePolicy::DEFAULT_FALLBACK_REPLACEMENT,
267                    CharsetEncodeError::new(target.charset(), kind, 0),
268                );
269            }
270        }
271    }
272}
273
274impl<D, E> BufferedTranscoder<D::Unit, E::Unit> for CharsetConverter<D, E>
275where
276    D: CharsetCodec,
277    E: CharsetEncodeProbe,
278{
279    type Error = CharsetConvertError;
280
281    /// Returns the target-side upper bound for converted output units.
282    #[inline(always)]
283    fn max_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
284        self.engine.max_output_len(input_len)
285    }
286
287    /// Returns the maximum target units needed to finalize pending conversion
288    /// state.
289    #[inline(always)]
290    fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
291        self.engine.max_finish_output_len()
292    }
293
294    /// Clears any pending decoded character.
295    #[inline(always)]
296    fn reset(&mut self) {
297        self.engine.reset();
298    }
299
300    /// Converts source units to target units through the configured decoder and
301    /// encoder.
302    ///
303    /// # Errors
304    ///
305    /// Returns [`CharsetConvertError::Decode`] when `input_index` is outside
306    /// the source input buffer or source decoding fails. Returns
307    /// [`CharsetConvertError::Encode`] when target encoding fails.
308    #[inline(always)]
309    fn transcode(
310        &mut self,
311        input: &[D::Unit],
312        input_index: usize,
313        output: &mut [E::Unit],
314        output_index: usize,
315    ) -> Result<TranscodeProgress, Self::Error> {
316        self.engine
317            .transcode(input, input_index, output, output_index)
318    }
319
320    /// Finalizes internally retained decoded characters and policy hook state.
321    ///
322    /// # Parameters
323    ///
324    /// - `output`: Complete output slice visible to the converter.
325    /// - `output_index`: Absolute output index where writing starts.
326    ///
327    /// # Returns
328    ///
329    /// Returns the number of target units written during finalization.
330    ///
331    /// # Errors
332    ///
333    /// Returns [`FinishError`] when `output_index` is invalid, when output
334    /// capacity is insufficient, or when encoding pending or final decoded
335    /// characters violates target charset policy.
336    #[inline(always)]
337    fn finish(
338        &mut self,
339        output: &mut [E::Unit],
340        output_index: usize,
341    ) -> Result<usize, FinishError<Self::Error>> {
342        self.engine.finish(output, output_index)
343    }
344}
345
346impl<D, E> BufferedConverter<D::Unit, E::Unit> for CharsetConverter<D, E>
347where
348    D: CharsetCodec,
349    E: CharsetEncodeProbe,
350{
351}