qubit_text_codec/codec/charset_decoder.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 crate::{
11 CharsetDecodeError,
12 CharsetDecodeErrorKind,
13 Coder,
14 CoderProgress,
15 CoderStatus,
16};
17
18use super::{
19 charset_codec::CharsetCodec,
20 decode_status::DecodeStatus,
21 malformed_action::MalformedAction,
22};
23
24/// Converts units of one charset into Unicode scalar values.
25///
26/// `CharsetDecoder` wraps a low-level [`CharsetCodec`] and applies the
27/// configured [`MalformedAction`] whenever the codec reports malformed input.
28///
29/// # Type Parameters
30///
31/// - `C`: Low-level charset codec used to decode source storage units into one
32/// Unicode scalar value.
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct CharsetDecoder<C>
35where
36 C: CharsetCodec,
37{
38 /// Low-level codec used for source decoding.
39 codec: C,
40 /// Action used for malformed input units.
41 malformed_action: MalformedAction,
42 /// Replacement character used by [`MalformedAction::Replace`].
43 replacement: char,
44}
45
46impl<C> CharsetDecoder<C>
47where
48 C: CharsetCodec,
49{
50 /// Default replacement character used when malformed input is replaced.
51 pub const DEFAULT_REPLACEMENT: char = '\u{fffd}';
52
53 /// Creates a decoder with default replacement policy.
54 ///
55 /// # Parameters
56 ///
57 /// - `codec`: Low-level charset codec used to decode input units.
58 ///
59 /// # Returns
60 ///
61 /// Returns a decoder whose malformed action is [`MalformedAction::Replace`]
62 /// and whose replacement character is `U+FFFD`.
63 #[must_use]
64 #[inline]
65 pub const fn new(codec: C) -> Self {
66 Self {
67 codec,
68 malformed_action: MalformedAction::Replace,
69 replacement: Self::DEFAULT_REPLACEMENT,
70 }
71 }
72
73 /// Creates a decoder with a custom replacement character.
74 ///
75 /// This method performs no codec-level validation because malformed-input
76 /// replacement for decoding writes directly to the output `char` buffer.
77 ///
78 /// # Parameters
79 ///
80 /// - `replacement`: Replacement character for malformed sequences.
81 ///
82 /// # Returns
83 ///
84 /// Returns a new decoder configured with the provided replacement.
85 #[inline]
86 pub fn with_replacement(mut self, replacement: char) -> Self {
87 self.replacement = replacement;
88 self
89 }
90
91 /// Returns the wrapped low-level codec.
92 ///
93 /// # Returns
94 ///
95 /// Returns a shared reference to the configured codec.
96 #[must_use]
97 #[inline]
98 pub const fn codec(&self) -> &C {
99 &self.codec
100 }
101
102 /// Returns a mutable reference to the wrapped codec.
103 ///
104 /// # Returns
105 ///
106 /// Returns a mutable reference to the configured codec.
107 #[must_use]
108 #[inline]
109 pub fn codec_mut(&mut self) -> &mut C {
110 &mut self.codec
111 }
112
113 /// Returns the configured malformed-input action.
114 ///
115 /// # Returns
116 ///
117 /// Returns the action used when source input is malformed.
118 #[must_use]
119 #[inline]
120 pub const fn malformed_action(&self) -> MalformedAction {
121 self.malformed_action
122 }
123
124 /// Sets the malformed-input action.
125 ///
126 /// # Parameters
127 ///
128 /// - `action`: New policy for malformed input units.
129 #[inline]
130 pub fn set_malformed_action(&mut self, action: MalformedAction) {
131 self.malformed_action = action;
132 }
133
134 /// Returns the configured replacement character.
135 ///
136 /// # Returns
137 ///
138 /// Returns the character emitted when [`MalformedAction::Replace`] is used.
139 #[must_use]
140 #[inline]
141 pub const fn replacement(&self) -> char {
142 self.replacement
143 }
144
145 /// Sets the replacement character.
146 ///
147 /// # Parameters
148 ///
149 /// - `replacement`: New replacement character used by replace policy.
150 #[inline]
151 pub fn set_replacement(&mut self, replacement: char) {
152 self.replacement = replacement;
153 }
154}
155
156impl<C> Coder<C::Unit, char> for CharsetDecoder<C>
157where
158 C: CharsetCodec,
159{
160 type Error = CharsetDecodeError;
161
162 /// Returns the maximum number of characters decoded from `input_len` units.
163 #[inline]
164 fn max_output_len(&self, input_len: usize) -> Option<usize> {
165 Some(input_len)
166 }
167
168 /// Decodes source units into Unicode scalar values while applying malformed policy.
169 fn convert(
170 &mut self,
171 input: &[C::Unit],
172 input_index: usize,
173 output: &mut [char],
174 output_index: usize,
175 ) -> Result<CoderProgress, Self::Error> {
176 if input_index > input.len() {
177 let kind = CharsetDecodeErrorKind::MalformedSequence { value: None };
178 return Err(CharsetDecodeError::new(self.codec.charset(), kind, input_index));
179 }
180 if output_index > output.len() {
181 let status = CoderStatus::NeedOutput {
182 output_index,
183 required: 1,
184 available: 0,
185 };
186 return Ok(CoderProgress::new(status, 0, 0));
187 }
188
189 let mut input_cursor = input_index;
190 let mut output_cursor = output_index;
191 while input_cursor < input.len() {
192 if output_cursor == output.len() {
193 let status = CoderStatus::NeedOutput {
194 output_index: output_cursor,
195 required: 1,
196 available: 0,
197 };
198 return Ok(CoderProgress::new(
199 status,
200 input_cursor - input_index,
201 output_cursor - output_index,
202 ));
203 }
204 match self.codec.decode_one(input, input_cursor) {
205 Ok(DecodeStatus::Complete { value, consumed }) => {
206 output[output_cursor] = value;
207 input_cursor += consumed;
208 output_cursor += 1;
209 }
210 Ok(DecodeStatus::NeedMore { required, available }) => {
211 let needed = required.saturating_sub(input_cursor);
212 let status = CoderStatus::NeedInput {
213 input_index: input_cursor,
214 required: needed,
215 available,
216 };
217 return Ok(CoderProgress::new(
218 status,
219 input_cursor - input_index,
220 output_cursor - output_index,
221 ));
222 }
223 Err(error)
224 if matches!(
225 error.kind(),
226 CharsetDecodeErrorKind::MalformedSequence { .. }
227 | CharsetDecodeErrorKind::InvalidCodePoint { .. }
228 ) =>
229 {
230 let skip = malformed_skip(input_cursor, input.len(), error.index());
231 match self.malformed_action {
232 MalformedAction::Report => return Err(error),
233 MalformedAction::Ignore => {
234 input_cursor += skip;
235 }
236 MalformedAction::Replace => {
237 output[output_cursor] = self.replacement;
238 input_cursor += skip;
239 output_cursor += 1;
240 }
241 }
242 }
243 Err(error) => return Err(error),
244 }
245 }
246 Ok(CoderProgress::complete(
247 input_cursor - input_index,
248 output_cursor - output_index,
249 ))
250 }
251}
252
253/// Calculates how many malformed input units should be skipped.
254///
255/// # Parameters
256///
257/// - `input_index`: Absolute index where decoding of the current character started.
258/// - `input_len`: Length of the complete input slice.
259/// - `error_index`: Absolute index reported by the low-level codec.
260///
261/// # Returns
262///
263/// Returns at least one unit when input remains. When the codec reports an error
264/// after the start index, the skipped range includes the reported failing unit.
265#[inline]
266fn malformed_skip(input_index: usize, input_len: usize, error_index: usize) -> usize {
267 let available = input_len.saturating_sub(input_index);
268 let end = error_index.saturating_add(1).min(input_len);
269 end.saturating_sub(input_index).max(1).min(available)
270}