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}