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}